Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c58ea3637f | ||
|
|
3eaea160f4 | ||
|
|
c9b14056c5 | ||
|
|
d3e32b344d | ||
|
|
0659493637 | ||
|
|
08ad9a4de9 | ||
|
|
fbaf28af5c | ||
|
|
8938fe3785 | ||
|
|
163104e14f | ||
|
|
9db3a4beeb | ||
|
|
3558626911 | ||
|
|
a783fc1817 | ||
|
|
90fad0f277 | ||
|
|
4e41215a87 | ||
|
|
6a49e1aa61 | ||
|
|
f039ba3fba | ||
|
|
116a09617e | ||
|
|
bc62843844 | ||
|
|
aca98b8a5e | ||
|
|
409c931093 | ||
|
|
4c8963a487 | ||
|
|
c480898d0d | ||
|
|
c44a913c2d | ||
|
|
4708890a27 | ||
|
|
fa3821adb9 | ||
|
|
e69a8b6240 | ||
|
|
9f64663bd0 | ||
|
|
29a7384233 | ||
|
|
d6cfe3db5d | ||
|
|
8c9b9fdb9b | ||
|
|
6ccafd9779 | ||
|
|
b5222f8010 | ||
|
|
e2ff222c4f | ||
|
|
1b822cb814 | ||
|
|
8961a72d2d | ||
|
|
576181d93d | ||
|
|
eef62138a4 | ||
|
|
c793c317fd | ||
|
|
a4e4e28f97 | ||
|
|
4452773563 | ||
|
|
7aea5197d8 | ||
|
|
9f39dc2bf9 | ||
|
|
330eaaf205 | ||
|
|
904ae93526 | ||
|
|
699f5537b0 | ||
|
|
abce6f1c62 | ||
|
|
4d93815c36 | ||
|
|
5b00193876 | ||
|
|
0b1c0b5c5a | ||
|
|
ce21694856 | ||
|
|
2ea67f9ea8 | ||
|
|
2ae6e3a603 | ||
|
|
06ce754fcc | ||
|
|
6791a0abea | ||
|
|
5501e931f7 | ||
|
|
b1aee19ac6 | ||
|
|
2b982bc39c | ||
|
|
8237004d5c | ||
|
|
9559005bbc | ||
|
|
46556eb6cf | ||
|
|
64ecce4f25 | ||
|
|
77bf56066c | ||
|
|
6fe6fe1af3 | ||
|
|
780cbef3cf | ||
|
|
8aab310366 | ||
|
|
9f232d981b | ||
|
|
1efd80cd17 | ||
|
|
1701dcef90 | ||
|
|
55dbe3b65e | ||
|
|
fed1138b85 | ||
|
|
53a98a720a | ||
|
|
b69ae4b3c9 | ||
|
|
092b07da86 | ||
|
|
3cdbc77386 | ||
|
|
f57b8e1534 | ||
|
|
c8f11d63b5 | ||
|
|
3d929a1db0 | ||
|
|
2fe3fd13a1 | ||
|
|
f33f502267 | ||
|
|
7c24c58007 | ||
|
|
596634ac41 | ||
|
|
0c8271db49 | ||
|
|
f3f4dcaf9f | ||
|
|
64718444e2 | ||
|
|
ea75b99e2b | ||
|
|
564113e053 | ||
|
|
b3de9fa6db | ||
|
|
1f054097f3 | ||
|
|
a250987508 | ||
|
|
21c99d0574 | ||
|
|
0b6fd2e8b2 | ||
|
|
4226e64245 | ||
|
|
a374c6c5af | ||
|
|
454e0749c2 | ||
|
|
59264f1f5b | ||
|
|
16ef64f8bf | ||
|
|
a0f93cdcc4 | ||
|
|
5b911db9cf | ||
|
|
560c0ce3a1 | ||
|
|
94958c2489 | ||
|
|
6426573f45 | ||
|
|
5ab59506aa | ||
|
|
096707d736 | ||
|
|
967d3f59e6 | ||
|
|
9a70816dad | ||
|
|
8e7e4f16e5 | ||
|
|
a9a2a8e3c1 | ||
|
|
92a1bfcf2e | ||
|
|
be38252e37 | ||
|
|
0b590dacd6 | ||
|
|
1faa41eac1 | ||
|
|
fa5eb33cdb | ||
|
|
3494f6b5b0 | ||
|
|
58fc09ff0e | ||
|
|
a47e4905e6 | ||
|
|
1252c69ccc | ||
|
|
be3dd80171 | ||
|
|
2db34c4955 | ||
|
|
8854430df4 | ||
|
|
26e771d075 | ||
|
|
fd13893687 | ||
|
|
5ef8001413 | ||
|
|
e2d1420ff7 | ||
|
|
cf1484d7fd | ||
|
|
e12f007597 | ||
|
|
3a4f9e235d | ||
|
|
7b05c8a1b2 | ||
|
|
b45167e00b | ||
|
|
bc42be2074 | ||
|
|
bc26cc16c1 | ||
|
|
80468eeb4c | ||
|
|
69b11a1a48 | ||
|
|
98d7c25bd5 | ||
|
|
6d1f12cb7b | ||
|
|
4500485e79 | ||
|
|
b4bd92d62c | ||
|
|
1a01e9634f | ||
|
|
e35a9c1e90 | ||
|
|
1dd113b38a | ||
|
|
536eeb4db0 | ||
|
|
3f3f6abfb9 | ||
|
|
c961408bf7 | ||
|
|
6fd6003d6c | ||
|
|
d407bf561f | ||
|
|
a1a8d1abae | ||
|
|
f34d0a255c | ||
|
|
a2cd1ab29c | ||
|
|
99550840d2 | ||
|
|
b0f6aed4a1 | ||
|
|
878e3fc52d | ||
|
|
f04679a42e | ||
|
|
6ebcf79358 | ||
|
|
d4acf2fa45 | ||
|
|
ddf2b7d091 | ||
|
|
5684f95c8e | ||
|
|
cd3dcc52b2 | ||
|
|
56a7195487 | ||
|
|
7b66cd026a | ||
|
|
235457ccdd | ||
|
|
eaa3a63ddf | ||
|
|
8883dc17a7 |
@@ -25,4 +25,4 @@ When writing benchmark instructions:
|
|||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
|
||||||
- Benchmark sources import compiled modules from `../../out/common/` and `../../out/browser/` (run `npm run build` first).
|
- Keep `NODE_PATH=./out` (handled by the npm script).
|
||||||
|
|||||||
@@ -6,14 +6,19 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 22.x
|
- name: Use Node.js 22.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -69,7 +74,7 @@ jobs:
|
|||||||
./addons/addon-webgl/out/* \
|
./addons/addon-webgl/out/* \
|
||||||
./addons/addon-webgl/out-*st/*
|
./addons/addon-webgl/out-*st/*
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
path: compressed-build.zip
|
path: compressed-build.zip
|
||||||
@@ -79,9 +84,11 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 22.x
|
- name: Use Node.js 22.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -100,16 +107,18 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 22.x
|
- name: Use Node.js 22.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
- name: Unzip artifacts
|
- name: Unzip artifacts
|
||||||
@@ -129,6 +138,7 @@ jobs:
|
|||||||
exit $EXIT_CODE
|
exit $EXIT_CODE
|
||||||
|
|
||||||
test-unit:
|
test-unit:
|
||||||
|
needs: build
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -136,21 +146,18 @@ jobs:
|
|||||||
runs-on: [ubuntu, macos, windows]
|
runs-on: [ubuntu, macos, windows]
|
||||||
runs-on: ${{ matrix.runs-on }}-latest
|
runs-on: ${{ matrix.runs-on }}-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}.x
|
node-version: ${{ matrix.node-version }}.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
npm ci
|
npm ci
|
||||||
- name: Wait for build job
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
job: build
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
- name: Unzip artifacts
|
- name: Unzip artifacts
|
||||||
@@ -166,6 +173,7 @@ jobs:
|
|||||||
run: npm run test-unit --forbid-only
|
run: npm run test-unit --forbid-only
|
||||||
|
|
||||||
test-integration:
|
test-integration:
|
||||||
|
needs: build
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -174,9 +182,11 @@ jobs:
|
|||||||
browser: [chromium, firefox, webkit]
|
browser: [chromium, firefox, webkit]
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}.x
|
node-version: ${{ matrix.node-version }}.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -185,12 +195,7 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
- name: Install playwright
|
- name: Install playwright
|
||||||
run: npx playwright install --with-deps ${{ matrix.browser }}
|
run: npx playwright install --with-deps ${{ matrix.browser }}
|
||||||
- name: Wait for build job
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
job: build
|
|
||||||
- uses: actions/download-artifact@v4
|
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
- name: Unzip artifacts
|
- name: Unzip artifacts
|
||||||
@@ -240,9 +245,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
node-version: [22]
|
node-version: [22]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}.x
|
node-version: ${{ matrix.node-version }}.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -251,7 +258,7 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
- name: Install playwright
|
- name: Install playwright
|
||||||
run: npx playwright install
|
run: npx playwright install
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||||
with:
|
with:
|
||||||
name: build-artifacts
|
name: build-artifacts
|
||||||
- name: Unzip artifacts
|
- name: Unzip artifacts
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ on:
|
|||||||
schedule:
|
schedule:
|
||||||
- cron: '41 17 * * 0'
|
- cron: '41 17 * * 0'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
analyze:
|
analyze:
|
||||||
name: Analyze
|
name: Analyze
|
||||||
@@ -24,12 +29,14 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- .github/workflows/copilot-setup-steps.yml
|
- .github/workflows/copilot-setup-steps.yml
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
copilot-setup-steps:
|
copilot-setup-steps:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -18,9 +21,11 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 22.x
|
- name: Use Node.js 22.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|||||||
@@ -6,13 +6,18 @@ on:
|
|||||||
# without running tests to save time to publish
|
# without running tests to save time to publish
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js 22.x
|
- name: Use Node.js 22.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||||
with:
|
with:
|
||||||
node-version: 22.x
|
node-version: 22.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"import",
|
||||||
"typescript"
|
"typescript"
|
||||||
],
|
],
|
||||||
"categories": {
|
"categories": {
|
||||||
@@ -104,7 +105,8 @@
|
|||||||
"typescript/no-wrapper-object-types": "error",
|
"typescript/no-wrapper-object-types": "error",
|
||||||
"typescript/prefer-as-const": "error",
|
"typescript/prefer-as-const": "error",
|
||||||
"typescript/prefer-namespace-keyword": "error",
|
"typescript/prefer-namespace-keyword": "error",
|
||||||
"typescript/triple-slash-reference": "error"
|
"typescript/triple-slash-reference": "error",
|
||||||
|
"import/no-cycle": "error"
|
||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ The preferred way to report bugs or request features is to use
|
|||||||
|
|
||||||
It's pretty common for maintainers of large open source projects to suffer from burnout, especially when needing to triage a large number of incoming issues instead of actually building things. Here are some of the steps we take to try mitigate this:
|
It's pretty common for maintainers of large open source projects to suffer from burnout, especially when needing to triage a large number of incoming issues instead of actually building things. Here are some of the steps we take to try mitigate this:
|
||||||
|
|
||||||
- Support questions live in [GH discussions](https://github.com/xtermjs/xterm.js/discussions), issues may be transfered there without further comment and core maintainers may or may not participate in discussions.
|
- Support questions live in [GH discussions](https://github.com/xtermjs/xterm.js/discussions), issues may be transferred there without further comment and core maintainers may or may not participate in discussions.
|
||||||
- Issues are strictly for well defined features or bugs that are _actionable_.
|
- Issues are strictly for well defined features or bugs that are _actionable_.
|
||||||
- Sometimes features are out of scope. A common example of this is a niche feature that the pricipal implementation ([VS Code](https://code.visualstudio.com/)) won't leverage and therefore would be difficult to maintain and likely suffer from bitrot. The reporter may not agree with this, but you could always create an addon if that works or maintain your own fork if it comes to that.
|
- Sometimes features are out of scope. A common example of this is a niche feature that the pricipal implementation ([VS Code](https://code.visualstudio.com/)) won't leverage and therefore would be difficult to maintain and likely suffer from bitrot. The reporter may not agree with this, but you could always create an addon if that works or maintain your own fork if it comes to that.
|
||||||
- If a feature does not have a clear way forward or needs more discussion it may be closed ro moved to a discussion.
|
- If a feature does not have a clear way forward or needs more discussion it may be closed ro moved to a discussion.
|
||||||
|
|||||||
@@ -254,6 +254,13 @@ Xterm.js is used in many world-class applications to provide great terminal expe
|
|||||||
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
|
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
|
||||||
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
||||||
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
|
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
|
||||||
|
- [**Cate**](https://github.com/0-AI-UG/cate): Open source desktop IDE on an infinite zoomable canvas. Editor, browser, and terminal panels float in spatial space; the terminals are xterm.js with the WebGL addon, backed by node-pty.
|
||||||
|
- [**OmnyShell PWA**](https://github.com/OmnyGrid/omnyshell_web): Web client for [**OmnyShell**](https://github.com/OmnyGrid/omnyshell), featuring remote terminals, a built-in IDE, AI-assisted workflows, and detachable/resumable sessions for seamless browser-based access to nodes.
|
||||||
|
- [**kmux**](https://github.com/kkd927/kmux): A keyboard-centric terminal workspace for running parallel AI coding agents side by side on macOS and Linux.
|
||||||
|
- [**LynxHub**](https://github.com/KindaBrazy/LynxHub): Cross-platform, extensible terminal and browser for AI management.
|
||||||
|
- [**Codeman**](https://github.com/Ark0N/Codeman): Self-hosted mission control for AI coding agents, streaming live tmux sessions to browser and phone.
|
||||||
|
- [**JSVIsion**](https://github.com/blendsdk/jsvision): A Turbo Vision-Style TUI SDK for TypeScript. We use xterm.js in our docs to show live JSVision examples running in a web terminal.
|
||||||
|
- [**BitFun**](https://github.com/GCWing/BitFun): Open-source desktop AI agent whose integrated terminals and read-only terminal output views use xterm.js with the Fit, Web Links, and WebGL addons.
|
||||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||||
|
|
||||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => {
|
|||||||
});
|
});
|
||||||
test.afterAll(async () => await ctx.page.close());
|
test.afterAll(async () => await ctx.page.close());
|
||||||
|
|
||||||
test.describe('Search Tests', () => {
|
test.describe('AttachAddon', () => {
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
test.beforeEach(async () => {
|
||||||
await ctx.proxy.reset();
|
await ctx.proxy.reset();
|
||||||
|
|||||||
@@ -78,13 +78,43 @@ export class BrowserClipboardProvider implements IClipboardProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: Once the base64 handling on Uint8Arrays is more common,
|
||||||
|
* remove the btoa/atob fallbacks below.
|
||||||
|
*/
|
||||||
|
interface IUint8ArrayB64 extends Uint8Array {
|
||||||
|
toBase64(): string;
|
||||||
|
}
|
||||||
|
interface IUint8ArrayB64Ctor extends Uint8ArrayConstructor {
|
||||||
|
fromBase64(s: string): IUint8ArrayB64;
|
||||||
|
}
|
||||||
|
|
||||||
export class Base64 implements IBase64 {
|
export class Base64 implements IBase64 {
|
||||||
public encodeText(data: string): string {
|
public encodeText(data: string): string {
|
||||||
return btoa(data);
|
const bytes = new TextEncoder().encode(data) as IUint8ArrayB64;
|
||||||
|
if (bytes.toBase64 !== undefined) {
|
||||||
|
return bytes.toBase64();
|
||||||
|
}
|
||||||
|
let bin = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
bin += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
return btoa(bin);
|
||||||
}
|
}
|
||||||
public decodeText(data: string): string {
|
public decodeText(data: string): string {
|
||||||
|
if ((Uint8Array as IUint8ArrayB64Ctor).fromBase64 !== undefined) {
|
||||||
|
try {
|
||||||
|
return new TextDecoder().decode((Uint8Array as IUint8ArrayB64Ctor).fromBase64(data));
|
||||||
|
} catch {}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
return atob(data);
|
const bin = atob(data);
|
||||||
|
const bytes = new Uint8Array(bin.length);
|
||||||
|
for (let i = 0; i < bin.length; ++i) {
|
||||||
|
bytes[i] = bin.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return new TextDecoder().decode(bytes);
|
||||||
} catch {}
|
} catch {}
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ test.describe('ClipboardAddon', () => {
|
|||||||
await ctx.page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
await ctx.page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
|
||||||
}
|
}
|
||||||
await ctx.page.evaluate(`
|
await ctx.page.evaluate(`
|
||||||
window.term.reset()
|
window.term.reset();
|
||||||
window.clipboard?.dispose();
|
window.clipboard?.dispose();
|
||||||
window.clipboard = new ClipboardAddon();
|
window.clipboard = new ClipboardAddon();
|
||||||
window.term.loadAddon(window.clipboard);
|
window.term.loadAddon(window.clipboard);
|
||||||
@@ -92,4 +92,22 @@ test.describe('ClipboardAddon', () => {
|
|||||||
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
|
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('non-ASCII', () => {
|
||||||
|
const testDataEncoded = '4oKsbWzDpMO8dMOf';
|
||||||
|
const testDataDecoded = '€mläütß';
|
||||||
|
test('write simple string', async () => {
|
||||||
|
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
|
||||||
|
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
|
||||||
|
});
|
||||||
|
test('read simple string', async () => {
|
||||||
|
await ctx.page.evaluate(`
|
||||||
|
window.data2 = [];
|
||||||
|
window.term.onData(e => data2.push(e));
|
||||||
|
`);
|
||||||
|
await ctx.page.evaluate((d) => window.navigator.clipboard.writeText(d), testDataDecoded);
|
||||||
|
await ctx.proxy.write(`\x1b]52;c;?\x07`);
|
||||||
|
deepEqual(await ctx.page.evaluate('window.data2'), [`\x1b]52;c;${testDataEncoded}\x07`]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ declare module '@xterm/addon-clipboard' {
|
|||||||
/**
|
/**
|
||||||
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
|
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
|
||||||
*
|
*
|
||||||
* Note this this clipboard provider does not implement own cut buffers as xterm does,
|
* Note that this clipboard provider does not implement own cut buffers as xterm does,
|
||||||
* but redirect the selection parameter always to navigator.clipboard.
|
* but redirect the selection parameter always to navigator.clipboard.
|
||||||
*/
|
*/
|
||||||
export class BrowserClipboardProvider implements IClipboardProvider{
|
export class BrowserClipboardProvider implements IClipboardProvider{
|
||||||
|
|||||||
@@ -74,14 +74,14 @@ export class FitAddon implements ITerminalAddon, IFitApi {
|
|||||||
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||||
|
|
||||||
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
||||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
const parentElementHeight = Math.max(0, parseInt(parentElementStyle.getPropertyValue('height'), 10) || 0);
|
||||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10) || 0);
|
||||||
const elementStyle = _getComputedStyle(this._terminal.element);
|
const elementStyle = _getComputedStyle(this._terminal.element);
|
||||||
const elementPadding = {
|
const elementPadding = {
|
||||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
top: parseInt(elementStyle.getPropertyValue('padding-top'), 10) || 0,
|
||||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom'), 10) || 0,
|
||||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
right: parseInt(elementStyle.getPropertyValue('padding-right'), 10) || 0,
|
||||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
left: parseInt(elementStyle.getPropertyValue('padding-left'), 10) || 0
|
||||||
};
|
};
|
||||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ terminal.loadAddon(imageAddon);
|
|||||||
therefore palette animations cannot be used.
|
therefore palette animations cannot be used.
|
||||||
|
|
||||||
- **SIXEL Raster Attributes Handling**
|
- **SIXEL Raster Attributes Handling**
|
||||||
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extend. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extend. Both is discouraged by the >30 years old specification.
|
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extent. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extent. Both is discouraged by the >30 years old specification.
|
||||||
|
|
||||||
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
|
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ terminal.loadAddon(imageAddon);
|
|||||||
Set by default, change it with `{iipSupport: true}`.
|
Set by default, change it with `{iipSupport: true}`.
|
||||||
|
|
||||||
The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
|
The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
|
||||||
- Supported formats: PNG, JPEG, GIF and QOI
|
- Supported formats: PNG, JPEG, GIF, QOI, WEBP and AVIF
|
||||||
- No animation support.
|
- No animation support.
|
||||||
- Image type hinting is not supported (always deducted from data header).
|
- Image type hinting is not supported (always deducted from data header).
|
||||||
- File download is not supported.
|
- File download is not supported.
|
||||||
@@ -112,6 +112,13 @@ terminal.loadAddon(imageAddon);
|
|||||||
- ReportCellSize sequence is supported.
|
- ReportCellSize sequence is supported.
|
||||||
- Hi-res display support is currently limited (image gets blurred).
|
- Hi-res display support is currently limited (image gets blurred).
|
||||||
|
|
||||||
|
- **Note on IIP image format support**
|
||||||
|
For all image formats except QOI the addon only does the dimension extraction and leaves
|
||||||
|
the image decoding to the underlying browser engine. Note that some formats are implemented
|
||||||
|
and enabled with different constraints by browser vendors, especially newer formats
|
||||||
|
(e.g. under Firefox you might need to change the setting _image.avif.compliance_strictness_ to 0
|
||||||
|
to get broader AVIF support).
|
||||||
|
|
||||||
- **Kitty Graphics Support (TGP)**
|
- **Kitty Graphics Support (TGP)**
|
||||||
Set by default, change it with `{kittySupport: true}`.
|
Set by default, change it with `{kittySupport: true}`.
|
||||||
|
|
||||||
@@ -149,9 +156,9 @@ The addon provides the following API endpoints to retrieve raw image data as can
|
|||||||
|
|
||||||
### Memory Usage
|
### Memory Usage
|
||||||
|
|
||||||
The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
|
The addon does most image processing in JavaScript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, let's look at the data flow and processing steps:
|
||||||
- Incomming image data chunk at `term.write` (terminal)
|
- Incoming image data chunk at `term.write` (terminal)
|
||||||
`term.write` might stock up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
|
`term.write` might stack up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
|
||||||
- Sequence Parser (terminal)
|
- Sequence Parser (terminal)
|
||||||
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
|
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
|
||||||
- Sequence Handler - Chunk Decoding (addon)
|
- Sequence Handler - Chunk Decoding (addon)
|
||||||
@@ -177,18 +184,18 @@ const totalActive = storageBytes + decodingBytes;
|
|||||||
```
|
```
|
||||||
|
|
||||||
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
|
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
|
||||||
Further note that the formulas above do not respect the Javascript object's overhead. Compared to the raw buffer needs the book keeping by these objects is rather small (<<5%).
|
Further note that the formulas above do not respect the JavaScript object's overhead. Compared to the raw buffer needs the bookkeeping by these objects is rather small (<<5%).
|
||||||
|
|
||||||
_Why should I care about memory usage at all?_
|
_Why should I care about memory usage at all?_
|
||||||
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the Javascript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
|
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the JavaScript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
|
||||||
|
|
||||||
_How can I adjust the memory usage?_
|
_How can I adjust the memory usage?_
|
||||||
- `pixelLimit`
|
- `pixelLimit`
|
||||||
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
|
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage only peaks temporarily, a rather high value should work even with multiple terminals in one page.
|
||||||
- `storageLimit`
|
- `storageLimit`
|
||||||
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
|
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for peak memory use during decoding.
|
||||||
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
|
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
|
||||||
Constructor settings. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
|
Constructor settings. This has only a small impact on peak memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
|
||||||
|
|
||||||
|
|
||||||
### Terminal Interaction
|
### Terminal Interaction
|
||||||
|
|||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 173 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 253 KiB |
|
After Width: | Height: | Size: 84 KiB |
@@ -39,7 +39,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||||
private _dec: Base64Decoder;
|
private _dec: Base64Decoder;
|
||||||
private _qoiDec: QoiDecoder;
|
private _qoiDec: QoiDecoder;
|
||||||
private _metrics = UNSUPPORTED_TYPE;
|
|
||||||
private _isMultipart = false;
|
private _isMultipart = false;
|
||||||
private _abortMulti = false;
|
private _abortMulti = false;
|
||||||
|
|
||||||
@@ -63,7 +62,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
this._aborted = false;
|
this._aborted = false;
|
||||||
this._metrics = UNSUPPORTED_TYPE;
|
|
||||||
this._hp.reset();
|
this._hp.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,14 +118,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
if (seqType === SequenceType.REPORTCELLSIZE) {
|
if (seqType === SequenceType.REPORTCELLSIZE) {
|
||||||
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
||||||
let width = CELL_SIZE_DEFAULT.width;
|
let w = CELL_SIZE_DEFAULT.width;
|
||||||
let height = CELL_SIZE_DEFAULT.height;
|
let h = CELL_SIZE_DEFAULT.height;
|
||||||
if (this._renderer.dimensions) {
|
if (this._renderer.dimensions) {
|
||||||
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
w = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
||||||
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
h = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
||||||
}
|
}
|
||||||
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
||||||
const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
||||||
this._coreTerminal.input(report, false);
|
this._coreTerminal.input(report, false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -154,17 +152,24 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
// early exit condition chain
|
// early exit condition chain
|
||||||
let cond: number | boolean;
|
let cond: number | boolean;
|
||||||
|
let metrics = UNSUPPORTED_TYPE;
|
||||||
if (cond = success) {
|
if (cond = success) {
|
||||||
if (cond = !this._dec.end()) {
|
if (cond = !this._dec.end()) {
|
||||||
this._metrics = imageType(this._dec.data8);
|
metrics = imageType(this._dec.data8);
|
||||||
if (cond = this._metrics.mime !== 'unsupported') {
|
if (cond = metrics.mime !== 'unsupported') {
|
||||||
w = this._metrics.width;
|
w = metrics.width;
|
||||||
h = this._metrics.height;
|
h = metrics.height;
|
||||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||||
[w, h] = this._resize(w, h).map(Math.floor);
|
[w, h] = this._resize(w, h).map(Math.floor);
|
||||||
cond = w && h && w * h < this._opts.pixelLimit;
|
cond = w && h && w * h < this._opts.pixelLimit;
|
||||||
|
} else {
|
||||||
|
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('IIP: unsupported image type');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('IIP: error during BASE64 decoding');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
@@ -173,7 +178,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let blob: Blob | ImageData;
|
let blob: Blob | ImageData;
|
||||||
if (this._metrics.mime === 'image/qoi') {
|
if (metrics.mime === 'image/qoi') {
|
||||||
const data = this._qoiDec.decode(this._dec.data8);
|
const data = this._qoiDec.decode(this._dec.data8);
|
||||||
blob = new ImageData(
|
blob = new ImageData(
|
||||||
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
||||||
@@ -190,13 +195,17 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
blob = new Blob([this._dec.data8], { type: metrics.mime });
|
||||||
}
|
}
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
||||||
.then(bm => {
|
.then(bm => {
|
||||||
this._storage.addImage(bm);
|
this._storage.addImage(bm);
|
||||||
return true;
|
return true;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,8 +231,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
private _dim(s: string, total: number, cdim: number): number {
|
private _dim(s: string, total: number, cdim: number): number {
|
||||||
if (s === 'auto') return 0;
|
if (s === 'auto') return 0;
|
||||||
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
|
if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
|
||||||
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
|
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
|
||||||
return parseInt(s) * cdim;
|
return parseInt(s, 10) * cdim;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,28 @@ const TEST_IMAGES: [string, IMetrics][] = [
|
|||||||
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
|
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
|
||||||
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }],
|
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }],
|
||||||
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }],
|
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }],
|
||||||
|
// VP8
|
||||||
|
['1.webp', { mime: 'image/webp', width: 550, height: 368 }],
|
||||||
|
['2.webp', { mime: 'image/webp', width: 550, height: 404 }],
|
||||||
|
['3.webp', { mime: 'image/webp', width: 1280, height: 720 }],
|
||||||
|
['4.webp', { mime: 'image/webp', width: 1024, height: 772 }],
|
||||||
|
['5.webp', { mime: 'image/webp', width: 1024, height: 752 }],
|
||||||
|
// VP8X
|
||||||
|
['1_webp_a.webp', { mime: 'image/webp', width: 400, height: 301 }],
|
||||||
|
['2_webp_a.webp', { mime: 'image/webp', width: 386, height: 395 }],
|
||||||
|
['3_webp_a.webp', { mime: 'image/webp', width: 800, height: 600 }],
|
||||||
|
['4_webp_a.webp', { mime: 'image/webp', width: 421, height: 163 }],
|
||||||
|
['5_webp_a.webp', { mime: 'image/webp', width: 300, height: 300 }],
|
||||||
|
// VP8L
|
||||||
|
['1_webp_ll.webp', { mime: 'image/webp', width: 400, height: 301 }],
|
||||||
|
['2_webp_ll.webp', { mime: 'image/webp', width: 386, height: 395 }],
|
||||||
|
['3_webp_ll.webp', { mime: 'image/webp', width: 800, height: 600 }],
|
||||||
|
['4_webp_ll.webp', { mime: 'image/webp', width: 421, height: 163 }],
|
||||||
|
['5_webp_ll.webp', { mime: 'image/webp', width: 300, height: 300 }],
|
||||||
|
// some AVIF test images
|
||||||
|
['fox.profile2.12bpc.yuv444.odd-width.odd-height.avif', { mime: 'image/avif', width: 1203, height: 799 }],
|
||||||
|
['hato.profile0.8bpc.yuv420.avif', { mime: 'image/avif', width: 3082, height: 2048 }],
|
||||||
|
['kimono.crop.avif', { mime: 'image/avif', width: 722, height: 1024 }],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
|
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'image/webp' | 'image/avif' | 'unsupported' | '';
|
||||||
|
|
||||||
export interface IMetrics {
|
export interface IMetrics {
|
||||||
mime: ImageType;
|
mime: ImageType;
|
||||||
@@ -19,16 +19,16 @@ export const UNSUPPORTED_TYPE: IMetrics = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function imageType(d: Uint8Array): IMetrics {
|
export function imageType(d: Uint8Array): IMetrics {
|
||||||
if (d.length < 24) {
|
if (d.length < 32) {
|
||||||
return UNSUPPORTED_TYPE;
|
return UNSUPPORTED_TYPE;
|
||||||
}
|
}
|
||||||
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
|
const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
|
||||||
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
||||||
// + first chunk must be IHDR
|
// + first chunk must be IHDR
|
||||||
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
||||||
return {
|
return {
|
||||||
mime: 'image/png',
|
mime: 'image/png',
|
||||||
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
||||||
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -41,7 +41,7 @@ export function imageType(d: Uint8Array): IMetrics {
|
|||||||
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
||||||
return {
|
return {
|
||||||
mime: 'image/gif',
|
mime: 'image/gif',
|
||||||
width: d[7] << 8 | d[6],
|
width: d[7] << 8 | d[6],
|
||||||
height: d[9] << 8 | d[8]
|
height: d[9] << 8 | d[8]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -49,13 +49,71 @@ export function imageType(d: Uint8Array): IMetrics {
|
|||||||
if (d32[0] === 0x66696F71) {
|
if (d32[0] === 0x66696F71) {
|
||||||
return {
|
return {
|
||||||
mime: 'image/qoi',
|
mime: 'image/qoi',
|
||||||
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
||||||
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
|
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// WEBP: RIFF | xxxx | WEBP | VP8x
|
||||||
|
if (d32[0] === 0x46464952 && d32[2] === 0x50424557 && (d32[3] & 0xFFFFFF) === 0x385056) {
|
||||||
|
switch (d[15]) {
|
||||||
|
case 0x58: // Extended WebP VP8X --> "X"
|
||||||
|
return {
|
||||||
|
mime: 'image/webp',
|
||||||
|
width: (d[24] | d[25] << 8 | d[26] << 16) + 1,
|
||||||
|
height: (d[27] | d[28] << 8 | d[29] << 16) + 1
|
||||||
|
};
|
||||||
|
case 0x4C: // Lossless WebP VP8L --> "L"
|
||||||
|
if (d[20] !== 0x2f) return UNSUPPORTED_TYPE;
|
||||||
|
const dim = d[21] | d[22] << 8 | d[23] << 16 | d[24] << 24;
|
||||||
|
return {
|
||||||
|
mime: 'image/webp',
|
||||||
|
width: (dim & 0x3FFF) + 1,
|
||||||
|
height: (dim >>> 14 & 0x3FFF) + 1
|
||||||
|
};
|
||||||
|
case 0x20: // Lossy WebP VP8 --> " "
|
||||||
|
if (d[23] !== 0x9d || d[24] !== 0x01 || d[25] !== 0x2a) return UNSUPPORTED_TYPE;
|
||||||
|
return {
|
||||||
|
mime: 'image/webp',
|
||||||
|
width: (d[26] | d[27] << 8) & 0x3FFF,
|
||||||
|
height: (d[28] | d[29] << 8) & 0x3FFF
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return UNSUPPORTED_TYPE;
|
||||||
|
}
|
||||||
|
// AVIF: Box size | ftyp | avif/avis
|
||||||
|
if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
|
||||||
|
let pos = -1;
|
||||||
|
// search for ispe box within first 1024 bytes
|
||||||
|
const limit = Math.min(d.length - 16, 1024);
|
||||||
|
for (let i = 8; i < limit; i++) {
|
||||||
|
// scan for ispe
|
||||||
|
if (d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
|
||||||
|
pos = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos !== -1) {
|
||||||
|
// dimensions are in BE at +8 (width) at +12 (height)
|
||||||
|
const width =
|
||||||
|
d[pos + 8] << 24 |
|
||||||
|
d[pos + 9] << 16 |
|
||||||
|
d[pos + 10] << 8 |
|
||||||
|
d[pos + 11];
|
||||||
|
const height =
|
||||||
|
d[pos + 12] << 24 |
|
||||||
|
d[pos + 13] << 16 |
|
||||||
|
d[pos + 14] << 8 |
|
||||||
|
d[pos + 15];
|
||||||
|
if (width > 0 && height > 0) {
|
||||||
|
return { mime: 'image/avif', width, height };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return UNSUPPORTED_TYPE;
|
||||||
|
}
|
||||||
return UNSUPPORTED_TYPE;
|
return UNSUPPORTED_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function jpgSize(d: Uint8Array): [number, number] {
|
function jpgSize(d: Uint8Array): [number, number] {
|
||||||
const len = d.length;
|
const len = d.length;
|
||||||
let i = 4;
|
let i = 4;
|
||||||
|
|||||||
@@ -5,12 +5,11 @@
|
|||||||
|
|
||||||
import { IDisposable } from '@xterm/xterm';
|
import { IDisposable } from '@xterm/xterm';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
import { ImageRenderer } from './ImageRenderer';
|
||||||
import {
|
import type {
|
||||||
ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
|
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts
|
||||||
IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
|
|
||||||
UnderlineStyle, IAddImageOpts
|
|
||||||
} from './Types';
|
} from './Types';
|
||||||
|
import type { IBufferLine } from 'common/buffer/Types';
|
||||||
|
import { CellData } from 'common/buffer/CellData';
|
||||||
|
|
||||||
// fallback default cell size
|
// fallback default cell size
|
||||||
export const CELL_SIZE_DEFAULT: ICellSize = {
|
export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||||
@@ -18,93 +17,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
|
|||||||
height: 14
|
height: 14
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
class ImageTileInfo {
|
||||||
* Extend extended attribute to also hold image tile information.
|
|
||||||
*
|
|
||||||
* Object definition is copied from base repo to fully mimick its behavior.
|
|
||||||
* Image data is added as additional public properties `imageId` and `tileId`.
|
|
||||||
*/
|
|
||||||
class ExtendedAttrsImage implements IExtendedAttrsImage {
|
|
||||||
private _ext: number = 0;
|
|
||||||
public get ext(): number {
|
|
||||||
if (this._urlId) {
|
|
||||||
return (
|
|
||||||
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
|
|
||||||
(this.underlineStyle << 26)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return this._ext;
|
|
||||||
}
|
|
||||||
public set ext(value: number) { this._ext = value; }
|
|
||||||
|
|
||||||
public get underlineStyle(): UnderlineStyle {
|
|
||||||
// Always return the URL style if it has one
|
|
||||||
if (this._urlId) {
|
|
||||||
return UnderlineStyle.DASHED;
|
|
||||||
}
|
|
||||||
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
|
|
||||||
}
|
|
||||||
public set underlineStyle(value: UnderlineStyle) {
|
|
||||||
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
|
|
||||||
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get underlineColor(): number {
|
|
||||||
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
||||||
}
|
|
||||||
public set underlineColor(value: number) {
|
|
||||||
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
||||||
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
|
||||||
}
|
|
||||||
|
|
||||||
public get underlineVariantOffset(): number {
|
|
||||||
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
|
|
||||||
if (val < 0) {
|
|
||||||
return val ^ 0xFFFFFFF8;
|
|
||||||
}
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
public set underlineVariantOffset(value: number) {
|
|
||||||
this._ext &= ~ExtFlags.VARIANT_OFFSET;
|
|
||||||
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _urlId: number = 0;
|
|
||||||
public get urlId(): number {
|
|
||||||
return this._urlId;
|
|
||||||
}
|
|
||||||
public set urlId(value: number) {
|
|
||||||
this._urlId = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
ext: number = 0,
|
|
||||||
urlId: number = 0,
|
|
||||||
public imageId = -1,
|
public imageId = -1,
|
||||||
public tileId = -1
|
public tileId = -1) {
|
||||||
) {
|
|
||||||
this._ext = ext;
|
|
||||||
this._urlId = urlId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clone(): IExtendedAttrsImage {
|
|
||||||
/**
|
|
||||||
* Technically we dont need a clone variant of ExtendedAttrsImage,
|
|
||||||
* as we never clone a cell holding image data.
|
|
||||||
* Note: Clone is only meant to be used by the InputHandler for
|
|
||||||
* sticky attributes, which is never the case for image data.
|
|
||||||
* We still provide a proper clone method to reflect the full ext attr
|
|
||||||
* state in case there are future use cases for clone.
|
|
||||||
*/
|
|
||||||
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isEmpty(): boolean {
|
|
||||||
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const EMPTY_ATTRS = new ExtendedAttrsImage();
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageStorage - extension of CoreTerminal:
|
* ImageStorage - extension of CoreTerminal:
|
||||||
@@ -126,6 +44,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
private _needsFullClear = false;
|
private _needsFullClear = false;
|
||||||
// hard limit of stored pixels (fallback limit of 10 MB)
|
// hard limit of stored pixels (fallback limit of 10 MB)
|
||||||
private _pixelLimit: number = 2500000;
|
private _pixelLimit: number = 2500000;
|
||||||
|
private _workCell: CellData = new CellData();
|
||||||
|
|
||||||
private _viewportMetrics: { cols: number, rows: number };
|
private _viewportMetrics: { cols: number, rows: number };
|
||||||
public onImageAdded: (() => void) | undefined;
|
public onImageAdded: (() => void) | undefined;
|
||||||
@@ -277,10 +196,10 @@ export class ImageStorage implements IDisposable {
|
|||||||
|
|
||||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||||
for (let row = 0; row < rows; ++row) {
|
for (let row = 0; row < rows; ++row) {
|
||||||
const line = buffer.lines.get(buffer.y + buffer.ybase);
|
const line = buffer.lines.get(buffer.y + buffer.ybase)!;
|
||||||
for (let col = 0; col < cols; ++col) {
|
for (let col = 0; col < cols; ++col) {
|
||||||
if (offset + col >= termCols) break;
|
if (offset + col >= termCols) break;
|
||||||
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
this._writeToCell(line, offset + col, imageId, row * cols + col);
|
||||||
tileCount++;
|
tileCount++;
|
||||||
}
|
}
|
||||||
if (opts.scrolling) {
|
if (opts.scrolling) {
|
||||||
@@ -427,55 +346,44 @@ export class ImageStorage implements IDisposable {
|
|||||||
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
||||||
|
|
||||||
// walk all cells in viewport and collect tiles found
|
// walk all cells in viewport and collect tiles found
|
||||||
// Note: We check _extendedAttrs directly (not just HAS_EXTENDED flag)
|
|
||||||
// because text writes clear the BG flag but leave image tile data intact.
|
|
||||||
// This lets top-layer images survive text overwrites (kitty C=1 behavior).
|
|
||||||
for (let row = start; row <= end; ++row) {
|
for (let row = start; row <= end; ++row) {
|
||||||
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
const line = buffer.lines.get(row + buffer.ydisp);
|
||||||
if (!line) return;
|
if (!line) return;
|
||||||
for (let col = 0; col < cols; ++col) {
|
for (let col = 0; col < cols; ++col) {
|
||||||
let e: IExtendedAttrsImage;
|
const e = line.getExtended(col)?.payload;
|
||||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
if (e instanceof ImageTileInfo) {
|
||||||
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
const imageId = e.imageId;
|
||||||
} else {
|
if (imageId === undefined || imageId === -1) {
|
||||||
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
||||||
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
e = maybeImg;
|
const imgSpec = this._images.get(imageId);
|
||||||
}
|
if (e.tileId !== -1) {
|
||||||
const imageId = e.imageId;
|
const startTile = e.tileId;
|
||||||
if (imageId === undefined || imageId === -1) {
|
const startCol = col;
|
||||||
continue;
|
let count = 1;
|
||||||
}
|
/**
|
||||||
const imgSpec = this._images.get(imageId);
|
* merge tiles to the right into a single draw call, if:
|
||||||
if (e.tileId !== -1) {
|
* - not at end of line
|
||||||
const startTile = e.tileId;
|
* - cell has same image id
|
||||||
const startCol = col;
|
* - cell has consecutive tile id
|
||||||
let count = 1;
|
*/
|
||||||
/**
|
while (++col < cols) {
|
||||||
* merge tiles to the right into a single draw call, if:
|
const nextE = line.getExtended(col)?.payload;
|
||||||
* - not at end of line
|
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||||
* - cell has same image id
|
break;
|
||||||
* - cell has consecutive tile id
|
}
|
||||||
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
count++;
|
||||||
*/
|
|
||||||
while (++col < cols) {
|
|
||||||
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
|
||||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
count++;
|
col--;
|
||||||
}
|
if (imgSpec) {
|
||||||
col--;
|
if (imgSpec.actual) {
|
||||||
if (imgSpec) {
|
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||||
if (imgSpec.actual) {
|
}
|
||||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
} else if (this._opts.showPlaceholder) {
|
||||||
|
placeholderCalls.push({ col: startCol, row, count });
|
||||||
}
|
}
|
||||||
} else if (this._opts.showPlaceholder) {
|
this._fullyCleared = false;
|
||||||
placeholderCalls.push({ col: startCol, row, count });
|
|
||||||
}
|
}
|
||||||
this._fullyCleared = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -513,9 +421,9 @@ export class ImageStorage implements IDisposable {
|
|||||||
const rows = buffer.lines.length;
|
const rows = buffer.lines.length;
|
||||||
const oldCol = this._viewportMetrics.cols - 1;
|
const oldCol = this._viewportMetrics.cols - 1;
|
||||||
for (let row = 0; row < rows; ++row) {
|
for (let row = 0; row < rows; ++row) {
|
||||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
const line = buffer.lines.get(row)!;
|
||||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
const e = line.getExtended(oldCol)?.payload;
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
if (e instanceof ImageTileInfo) {
|
||||||
const imageId = e.imageId;
|
const imageId = e.imageId;
|
||||||
if (imageId === undefined || imageId === -1) {
|
if (imageId === undefined || imageId === -1) {
|
||||||
continue;
|
continue;
|
||||||
@@ -532,7 +440,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
// expand only if right side is empty (nothing got wrapped from below)
|
// expand only if right side is empty (nothing got wrapped from below)
|
||||||
let hasData = false;
|
let hasData = false;
|
||||||
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
|
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
|
||||||
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
|
if (line.hasContent(rightCol)) {
|
||||||
hasData = true;
|
hasData = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -544,7 +452,7 @@ export class ImageStorage implements IDisposable {
|
|||||||
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
||||||
let lastTile = e.tileId;
|
let lastTile = e.tileId;
|
||||||
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
||||||
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
|
this._writeToCell(line, expandCol, imageId, ++lastTile);
|
||||||
imgSpec.tileCount++;
|
imgSpec.tileCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -558,10 +466,10 @@ export class ImageStorage implements IDisposable {
|
|||||||
*/
|
*/
|
||||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||||
const buffer = this._terminal._core.buffer;
|
const buffer = this._terminal._core.buffer;
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
const line = buffer.lines.get(y);
|
||||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
if (line) {
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
const e = line.getExtended(x)?.payload;
|
||||||
if (e.imageId && e.imageId !== -1) {
|
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
|
||||||
const orig = this._images.get(e.imageId)?.orig;
|
const orig = this._images.get(e.imageId)?.orig;
|
||||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||||
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
||||||
@@ -578,10 +486,10 @@ export class ImageStorage implements IDisposable {
|
|||||||
*/
|
*/
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||||
const buffer = this._terminal._core.buffer;
|
const buffer = this._terminal._core.buffer;
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
const line = buffer.lines.get(y);
|
||||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
if (line) {
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
const e = line.getExtended(x)?.payload;
|
||||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||||
const spec = this._images.get(e.imageId);
|
const spec = this._images.get(e.imageId);
|
||||||
if (spec) {
|
if (spec) {
|
||||||
return this._renderer.extractTile(spec, e.tileId);
|
return this._renderer.extractTile(spec, e.tileId);
|
||||||
@@ -609,31 +517,31 @@ export class ImageStorage implements IDisposable {
|
|||||||
return used - current;
|
return used - current;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
|
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void {
|
||||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
const workCell = this._workCell;
|
||||||
const old = line._extendedAttrs[x];
|
line.loadCell(x, workCell);
|
||||||
if (old) {
|
if (this._workCell.hasExtendedAttrs()) {
|
||||||
if (old.imageId !== undefined) {
|
const old = workCell.extended.payload;
|
||||||
// found an old ExtendedAttrsImage, since we know that
|
if (old instanceof ImageTileInfo) {
|
||||||
// they are always isolated instances (single cell usage),
|
// found an old ExtendedAttrsImage, since we know that
|
||||||
// we can re-use it and just update their id entries
|
// they are always isolated instances (single cell usage),
|
||||||
const oldSpec = this._images.get(old.imageId);
|
// we can re-use it and just update their id entries
|
||||||
if (oldSpec) {
|
const oldSpec = this._images.get(old.imageId);
|
||||||
// early eviction for in-viewport overwrites
|
if (oldSpec) {
|
||||||
oldSpec.tileCount--;
|
// early eviction for in-viewport overwrites
|
||||||
}
|
oldSpec.tileCount--;
|
||||||
old.imageId = imageId;
|
|
||||||
old.tileId = tileId;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
// found a plain ExtendedAttrs instance, clone it to new entry
|
old.imageId = imageId;
|
||||||
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
old.tileId = tileId;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// fall-through: always create new ExtendedAttrsImage entry
|
// fall-through: always create new ExtendedAttrs entry
|
||||||
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
|
const extattr = workCell.extended.clone();
|
||||||
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
extattr.payload = new ImageTileInfo(imageId, tileId);
|
||||||
|
workCell.extended = extattr;
|
||||||
|
workCell.updateExtended();
|
||||||
|
line.setCell(x, workCell);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _evictOnAlternate(): void {
|
private _evictOnAlternate(): void {
|
||||||
@@ -646,18 +554,16 @@ export class ImageStorage implements IDisposable {
|
|||||||
// re-count tiles on whole buffer
|
// re-count tiles on whole buffer
|
||||||
const buffer = this._terminal._core.buffer;
|
const buffer = this._terminal._core.buffer;
|
||||||
for (let y = 0; y < this._terminal.rows; ++y) {
|
for (let y = 0; y < this._terminal.rows; ++y) {
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
const line = buffer.lines.get(y);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
for (let x = 0; x < this._terminal.cols; ++x) {
|
for (let x = 0; x < this._terminal.cols; ++x) {
|
||||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
const payload = line.getExtended(x)?.payload;
|
||||||
const imgId = line._extendedAttrs[x]?.imageId;
|
if (payload instanceof ImageTileInfo) {
|
||||||
if (imgId) {
|
const spec = this._images.get(payload.imageId);
|
||||||
const spec = this._images.get(imgId);
|
if (spec) {
|
||||||
if (spec) {
|
spec.tileCount++;
|
||||||
spec.tileCount++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
|||||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||||
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
import type { IInputHandler } from 'common/Types';
|
||||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||||
@@ -47,6 +47,7 @@ export interface IResetHandler {
|
|||||||
reset(): void;
|
reset(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable */
|
||||||
/**
|
/**
|
||||||
* Stub into private interfaces.
|
* Stub into private interfaces.
|
||||||
* This should be kept in line with common libs.
|
* This should be kept in line with common libs.
|
||||||
@@ -54,19 +55,6 @@ export interface IResetHandler {
|
|||||||
* have a somewhat reliable testing against code changes in the core repo.
|
* have a somewhat reliable testing against code changes in the core repo.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// overloaded IExtendedAttrs to hold image refs
|
|
||||||
export interface IExtendedAttrsImage extends IExtendedAttrs {
|
|
||||||
imageId: number;
|
|
||||||
tileId: number;
|
|
||||||
clone(): IExtendedAttrsImage;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable */
|
|
||||||
export interface IBufferLineExt extends IBufferLine {
|
|
||||||
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
|
|
||||||
_data: Uint32Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IInputHandlerExt extends IInputHandler {
|
interface IInputHandlerExt extends IInputHandler {
|
||||||
_parser: IEscapeSequenceParser;
|
_parser: IEscapeSequenceParser;
|
||||||
_curAttrData: AttributeData;
|
_curAttrData: AttributeData;
|
||||||
@@ -77,6 +65,7 @@ interface IInputHandlerExt extends IInputHandler {
|
|||||||
};
|
};
|
||||||
onRequestReset(handler: () => void): IDisposable;
|
onRequestReset(handler: () => void): IDisposable;
|
||||||
}
|
}
|
||||||
|
/* eslint-enable */
|
||||||
|
|
||||||
export interface ICoreTerminalExt extends ITerminal {
|
export interface ICoreTerminalExt extends ITerminal {
|
||||||
_themeService: IThemeService | undefined;
|
_themeService: IThemeService | undefined;
|
||||||
@@ -88,8 +77,6 @@ export interface ICoreTerminalExt extends ITerminal {
|
|||||||
export interface ITerminalExt extends Terminal {
|
export interface ITerminalExt extends Terminal {
|
||||||
_core: ICoreTerminalExt;
|
_core: ICoreTerminalExt;
|
||||||
}
|
}
|
||||||
/* eslint-enable */
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Some storage definitions.
|
* Some storage definitions.
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
|||||||
cmd.deleteSelector = value;
|
cmd.deleteSelector = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const numValue = parseInt(value);
|
const numValue = parseInt(value, 10);
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||||
case KittyKey.ID: cmd.id = numValue; break;
|
case KittyKey.ID: cmd.id = numValue; break;
|
||||||
|
|||||||
@@ -435,6 +435,15 @@ test.describe('ImageAddon', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('text overwrite removes tiles', () => {
|
||||||
|
test('sixel', async () => {
|
||||||
|
await assertTextClearsTiles(SIXEL_SEQ_0);
|
||||||
|
});
|
||||||
|
test('iip', async () => {
|
||||||
|
await assertTextClearsTiles(TESTDATA_IIP[0][0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -472,3 +481,20 @@ async function getOrigSize(id: number): Promise<[number, number]> {
|
|||||||
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
||||||
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
|
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||||
|
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
|
||||||
|
await ctx.proxy.write('\x1b[H' + imageSeq);
|
||||||
|
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
||||||
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
|
await ctx.proxy.write('\x1b[2;6H#######');
|
||||||
|
for (let x = 5; x < 12; x++) {
|
||||||
|
strictEqual(await hasTileAtBufferCell(x, 1), false);
|
||||||
|
}
|
||||||
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
|
ok(await hasTileAtBufferCell(13, 1));
|
||||||
|
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -2213,6 +2213,21 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await pollFor(ctx.page, 'window._imageAddedCount', 2);
|
await pollFor(ctx.page, 'window._imageAddedCount', 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('text overwrite removes tiles', () => {
|
||||||
|
test('a=T placement', async () => {
|
||||||
|
await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
|
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
||||||
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
|
await ctx.proxy.write('\x1b[2;6H#######');
|
||||||
|
for (let x = 5; x < 12; x++) {
|
||||||
|
strictEqual(await hasTileAtBufferCell(x, 1), false);
|
||||||
|
}
|
||||||
|
ok(await hasTileAtBufferCell(0, 1));
|
||||||
|
ok(await hasTileAtBufferCell(13, 1));
|
||||||
|
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2262,3 +2277,7 @@ async function getPixels(col: number, row: number, x: number, y: number, w: numb
|
|||||||
return Array.from(ctx2d.getImageData(x, y, w, h).data);
|
return Array.from(ctx2d.getImageData(x, y, w, h).data);
|
||||||
}, [col, row, x, y, w, h]);
|
}, [col, row, x, y, w, h]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||||
|
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const CACHE_SIZE = 100000;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable ligature support for the provided Terminal instance. To function
|
* Enable ligature support for the provided Terminal instance. To function
|
||||||
* properly, this must be called after `open()` is called on the therminal. If
|
* properly, this must be called after `open()` is called on the terminal. If
|
||||||
* the font currently in use supports ligatures, the terminal will automatically
|
* the font currently in use supports ligatures, the terminal will automatically
|
||||||
* start to render them.
|
* start to render them.
|
||||||
* @param term Terminal instance from xterm.js
|
* @param term Terminal instance from xterm.js
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { DecorationManager } from './DecorationManager';
|
||||||
|
import { SearchEngine } from './SearchEngine';
|
||||||
|
import { SearchLineCache } from './SearchLineCache';
|
||||||
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
|
import type { ISearchDecorationOptions } from '@xterm/addon-search';
|
||||||
|
import type { IDecorationOptions } from '@xterm/xterm';
|
||||||
|
import { DisposableStore } from 'common/Lifecycle';
|
||||||
|
|
||||||
|
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||||
|
return new Promise(r => terminal.write(data, r));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('DecorationManager', () => {
|
||||||
|
let store: DisposableStore;
|
||||||
|
let terminal: Terminal;
|
||||||
|
let decorationManager: DecorationManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new DisposableStore();
|
||||||
|
terminal = store.add(new Terminal({ cols: 10, rows: 5 }));
|
||||||
|
decorationManager = store.add(new DecorationManager(terminal));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should split highlight decorations for a wrapped match', async () => {
|
||||||
|
await writeP(terminal, '0123456789abcde');
|
||||||
|
const searchEngine = new SearchEngine(terminal, store.add(new SearchLineCache(terminal)));
|
||||||
|
const match = searchEngine.find('9abc', 0, 0);
|
||||||
|
assert.ok(match);
|
||||||
|
|
||||||
|
const decorationOptions: IDecorationOptions[] = [];
|
||||||
|
const registerDecoration = terminal.registerDecoration.bind(terminal);
|
||||||
|
terminal.registerDecoration = (options: IDecorationOptions) => {
|
||||||
|
decorationOptions.push(options);
|
||||||
|
return registerDecoration(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: ISearchDecorationOptions = {
|
||||||
|
matchOverviewRuler: '#ff0000',
|
||||||
|
activeMatchColorOverviewRuler: '#00ff00'
|
||||||
|
};
|
||||||
|
decorationManager.createHighlightDecorations([match], options);
|
||||||
|
|
||||||
|
assert.strictEqual(decorationOptions.length, 2);
|
||||||
|
assert.strictEqual(decorationOptions[0].x, 9);
|
||||||
|
assert.strictEqual(decorationOptions[0].width, 1);
|
||||||
|
assert.strictEqual(decorationOptions[1].x, 0);
|
||||||
|
assert.strictEqual(decorationOptions[1].width, 3);
|
||||||
|
|
||||||
|
const withOverviewRuler = decorationOptions.filter(o => o.overviewRulerOptions !== undefined);
|
||||||
|
assert.strictEqual(withOverviewRuler.length, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only add one overview ruler marker per buffer line', async () => {
|
||||||
|
await writeP(terminal, 'abcdefghij');
|
||||||
|
const decorationOptions: IDecorationOptions[] = [];
|
||||||
|
const registerDecoration = terminal.registerDecoration.bind(terminal);
|
||||||
|
terminal.registerDecoration = (options: IDecorationOptions) => {
|
||||||
|
decorationOptions.push(options);
|
||||||
|
return registerDecoration(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: ISearchDecorationOptions = {
|
||||||
|
matchOverviewRuler: '#ff0000',
|
||||||
|
activeMatchColorOverviewRuler: '#00ff00'
|
||||||
|
};
|
||||||
|
decorationManager.createHighlightDecorations([
|
||||||
|
{ term: 'a', col: 0, row: 0, size: 1 },
|
||||||
|
{ term: 'f', col: 5, row: 0, size: 1 }
|
||||||
|
], options);
|
||||||
|
|
||||||
|
const withOverviewRuler = decorationOptions.filter(o => o.overviewRulerOptions !== undefined);
|
||||||
|
assert.strictEqual(withOverviewRuler.length, 1);
|
||||||
|
assert.strictEqual(withOverviewRuler[0].x, 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -142,12 +142,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
|||||||
}
|
}
|
||||||
prevResult = result;
|
prevResult = result;
|
||||||
results.push(prevResult);
|
results.push(prevResult);
|
||||||
result = this._engine.find(
|
const cols = this._terminal.cols;
|
||||||
term,
|
let nextCol = prevResult.col + prevResult.size;
|
||||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
|
let nextRow = prevResult.row;
|
||||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
|
if (nextCol >= cols) {
|
||||||
searchOptions
|
nextRow += Math.floor(nextCol / cols);
|
||||||
);
|
nextCol = nextCol % cols;
|
||||||
|
}
|
||||||
|
result = this._engine.find(term, nextRow, nextCol, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._resultTracker.updateResults(results, this._highlightLimit);
|
this._resultTracker.updateResults(results, this._highlightLimit);
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export class SearchEngine {
|
|||||||
this._terminal.clearSelection();
|
this._terminal.clearSelection();
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (startCol > this._terminal.cols) {
|
if (startCol >= this._terminal.cols) {
|
||||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -414,10 +414,24 @@ test.describe('Search Tests', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Regression tests', () => {
|
test.describe('Regression tests', () => {
|
||||||
|
test('should advance highlight-all scan by buffer match size for wide characters', async () => {
|
||||||
|
await ctx.page.evaluate(`
|
||||||
|
window.calls = [];
|
||||||
|
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||||
|
`);
|
||||||
|
await ctx.proxy.resize(3, 5);
|
||||||
|
await ctx.proxy.write('𝄞𝄞𝄞');
|
||||||
|
strictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
|
||||||
|
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
|
||||||
|
{ resultCount: 3, resultIndex: 0 }
|
||||||
|
]);
|
||||||
|
await ctx.proxy.resize(80, 24);
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('#2444 wrapped line content not being found', () => {
|
test.describe('#2444 wrapped line content not being found', () => {
|
||||||
let fixture: string;
|
let fixture: string;
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
fixture = (await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)))).toString();
|
fixture = (await new Promise<Buffer>((res, rej) => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => err ? rej(err) : res(data)))).toString();
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
fixture = fixture.replace(/\n/g, '\n\r');
|
fixture = fixture.replace(/\n/g, '\n\r');
|
||||||
}
|
}
|
||||||
@@ -501,7 +515,7 @@ test.describe('Search Tests', () => {
|
|||||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||||
`);
|
`);
|
||||||
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
||||||
await ctx.proxy.write('\\x1b[CHi Hi');
|
await ctx.proxy.write('\x1b[CHi Hi');
|
||||||
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||||
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
|
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
|
||||||
{ resultCount: 2, resultIndex: 1 }
|
{ resultCount: 2, resultIndex: 1 }
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
|
|
||||||
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||||
import { IAttributeData, IColor } from 'common/Types';
|
import { IColor } from 'common/Types';
|
||||||
|
import { IAttributeData } from 'common/buffer/Types';
|
||||||
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
||||||
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
||||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
|
// if there is colorful empty cell at line end, we must pad it back, or the color block
|
||||||
// will missing
|
// will missing
|
||||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||||
// use clear right to set background.
|
// use clear right to set background.
|
||||||
@@ -220,7 +221,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
// you can't use control sequence to move cursor to (x === row)
|
// you can't use control sequence to move cursor to (x === row)
|
||||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||||
// change background of the first wrapped cell also affects BCE
|
// change background of the first wrapped cell also affects BCE
|
||||||
// so we mark it as invalid to simply the process to determine line separator
|
// so we mark it as invalid to simplify the process to determine line separator
|
||||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||||
) {
|
) {
|
||||||
isValid = true;
|
isValid = true;
|
||||||
@@ -232,7 +233,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
|||||||
isNextRowFirstCharDoubleWidth &&
|
isNextRowFirstCharDoubleWidth &&
|
||||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||||
// change background of the first wrapped cell also affects BCE
|
// change background of the first wrapped cell also affects BCE
|
||||||
// so we mark it as invalid to simply the process to determine line separator
|
// so we mark it as invalid to simplify the process to determine line separator
|
||||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ test.describe('SerializeAddon', () => {
|
|||||||
const cols = 10;
|
const cols = 10;
|
||||||
const line = '+'.repeat(cols);
|
const line = '+'.repeat(cols);
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
|
sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags at the end, serialize will use \x1b[0m to clear instead of the specific disable sequence
|
||||||
sgr(INVERSE) + line,
|
sgr(INVERSE) + line,
|
||||||
sgr(BOLD) + line,
|
sgr(BOLD) + line,
|
||||||
sgr(UNDERLINED) + line,
|
sgr(UNDERLINED) + line,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*
|
*
|
||||||
* UnicodeVersionProvider for V15 with grapeme cluster handleing.
|
* UnicodeVersionProvider for V15 with grapheme cluster handling.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ function isUrl(urlString: string): boolean {
|
|||||||
|
|
||||||
export class LinkComputer {
|
export class LinkComputer {
|
||||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
||||||
|
const rex = new RegExp(regex.source, flags);
|
||||||
|
|
||||||
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
||||||
const line = lines.join('');
|
const line = lines.join('');
|
||||||
|
|||||||
@@ -7,13 +7,13 @@ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
|||||||
import type { WebLinksAddon as IWebLinksApi } from '@xterm/addon-web-links';
|
import type { WebLinksAddon as IWebLinksApi } from '@xterm/addon-web-links';
|
||||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||||
|
|
||||||
// consider everthing starting with http:// or https://
|
// consider everything starting with http:// or https://
|
||||||
// up to first whitespace, `"` or `'` as url
|
// up to first whitespace, `"` or `'` as url
|
||||||
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
||||||
// resembling the old (...)*([^:"\'\\s]) final path clause
|
// resembling the old (...)*([^:"\'\\s]) final path clause
|
||||||
// additionally exclude early + final:
|
// additionally exclude early + final:
|
||||||
// - unsafe from rfc3986: !*'()
|
// - unsafe from rfc3986: !*'()
|
||||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
|
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] for ipv6 addresses, also allow ~)
|
||||||
// also exclude as finals:
|
// also exclude as finals:
|
||||||
// - final interpunction like ,.!?
|
// - final interpunction like ,.!?
|
||||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-
|
|||||||
|
|
||||||
### Handling Context Loss
|
### Handling Context Loss
|
||||||
|
|
||||||
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
const terminal = new Terminal();
|
const terminal = new Terminal();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
|||||||
import { ReadonlyColorSet } from 'browser/Types';
|
import { ReadonlyColorSet } from 'browser/Types';
|
||||||
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
|
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
|
||||||
import { IDecorationService, IOptionsService } from 'common/services/Services';
|
import { IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
import { ICellData } from 'common/Types';
|
import { ICellData } from 'common/buffer/Types';
|
||||||
import { Terminal } from '@xterm/xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { rgba } from 'common/Color';
|
import { rgba } from 'common/Color';
|
||||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||||
|
|||||||
@@ -100,6 +100,8 @@ export class GlyphRenderer extends Disposable {
|
|||||||
private readonly _attributesBuffer: WebGLBuffer;
|
private readonly _attributesBuffer: WebGLBuffer;
|
||||||
|
|
||||||
private _atlas: ITextureAtlas | undefined;
|
private _atlas: ITextureAtlas | undefined;
|
||||||
|
private _lastSeenPageLayoutVersion: number = -1;
|
||||||
|
private _pageOverflowWarned: boolean = false;
|
||||||
private _activeBuffer: number = 0;
|
private _activeBuffer: number = 0;
|
||||||
private readonly _vertices: IVertices = {
|
private readonly _vertices: IVertices = {
|
||||||
count: 0,
|
count: 0,
|
||||||
@@ -213,14 +215,25 @@ export class GlyphRenderer extends Disposable {
|
|||||||
this.handleResize();
|
this.handleResize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call when a frame is being drawn. Returns whether the full model must be rebuilt before
|
||||||
|
* rendering this frame because the atlas page layout changed since this renderer last drew.
|
||||||
|
*/
|
||||||
public beginFrame(): boolean {
|
public beginFrame(): boolean {
|
||||||
return this._atlas ? this._atlas.beginFrame() : true;
|
if (!this._atlas) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this._atlas.pageLayoutVersion !== this._lastSeenPageLayoutVersion) {
|
||||||
|
this._lastSeenPageLayoutVersion = this._atlas.pageLayoutVersion;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
|
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
|
||||||
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
||||||
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
||||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
// slight slowdown is acceptable for the developer ergonomics provided as it's a one-off for
|
||||||
// each glyph.
|
// each glyph.
|
||||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
|
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
|
||||||
}
|
}
|
||||||
@@ -363,7 +376,13 @@ export class GlyphRenderer extends Disposable {
|
|||||||
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
||||||
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
||||||
// is detected by the same comparison.
|
// is detected by the same comparison.
|
||||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
// Clamp defensively in case the atlas unexpectedly exceeds texture capacity.
|
||||||
|
const pageCount = Math.min(this._atlas.pages.length, this._atlasTextures.length);
|
||||||
|
if (this._atlas.pages.length > this._atlasTextures.length && !this._pageOverflowWarned) {
|
||||||
|
this._pageOverflowWarned = true;
|
||||||
|
this._logService.warn(`Atlas page count (${this._atlas.pages.length}) exceeds the renderer's texture capacity (${this._atlasTextures.length}), some glyphs will not render correctly`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < pageCount; i++) {
|
||||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||||
}
|
}
|
||||||
@@ -375,6 +394,7 @@ export class GlyphRenderer extends Disposable {
|
|||||||
|
|
||||||
public setAtlas(atlas: ITextureAtlas): void {
|
public setAtlas(atlas: ITextureAtlas): void {
|
||||||
this._atlas = atlas;
|
this._atlas = atlas;
|
||||||
|
this._lastSeenPageLayoutVersion = -1;
|
||||||
this.invalidateAtlasTextures();
|
this.invalidateAtlasTextures();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,8 +409,9 @@ export class GlyphRenderer extends Disposable {
|
|||||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||||
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
||||||
gl.generateMipmap(gl.TEXTURE_2D);
|
|
||||||
this._atlasTextures[i].version = atlas.pages[i].version;
|
this._atlasTextures[i].version = atlas.pages[i].version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
private _overflowSizePage: AtlasPage | undefined;
|
private _overflowSizePage: AtlasPage | undefined;
|
||||||
|
|
||||||
private _tmpCanvas: HTMLCanvasElement;
|
private _tmpCanvas: HTMLCanvasElement;
|
||||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
// A temporary context that glyphs are drawn to before being transferred to the atlas.
|
||||||
private _tmpCtx: CanvasRenderingContext2D;
|
private _tmpCtx: CanvasRenderingContext2D;
|
||||||
|
|
||||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||||
@@ -132,12 +132,8 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _requestClearModel = false;
|
private _pageLayoutVersion = 0;
|
||||||
public beginFrame(): boolean {
|
public get pageLayoutVersion(): number { return this._pageLayoutVersion; }
|
||||||
const result = this._requestClearModel;
|
|
||||||
this._requestClearModel = false;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearTexture(): void {
|
public clearTexture(): void {
|
||||||
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
|
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
|
||||||
@@ -149,10 +145,15 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
this._cacheMap.clear();
|
this._cacheMap.clear();
|
||||||
this._cacheMapCombined.clear();
|
this._cacheMapCombined.clear();
|
||||||
this._didWarmUp = false;
|
this._didWarmUp = false;
|
||||||
|
|
||||||
|
// Invalidate renderer models so all texture pages are refreshed. The atlas may be shared, in
|
||||||
|
// which case the clearing renderer has cleared only its own model and every other owner still
|
||||||
|
// holds texture coords into the rows just wiped.
|
||||||
|
this._pageLayoutVersion++;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createNewPage(): AtlasPage {
|
private _createNewPage(): AtlasPage {
|
||||||
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
|
// Try merge the set of the 4 most used pages of the largest size. This is deferred to a
|
||||||
// microtask to ensure it does not interrupt textures that will be rendered in the current
|
// microtask to ensure it does not interrupt textures that will be rendered in the current
|
||||||
// animation frame which would result in blank rendered areas. This is actually not that
|
// animation frame which would result in blank rendered areas. This is actually not that
|
||||||
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
||||||
@@ -184,6 +185,8 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
||||||
// effectively reduce page count and merging would cause issues.
|
// effectively reduce page count and merging would cause issues.
|
||||||
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
||||||
|
// Evict instead of adding a page beyond the renderer's texture capacity.
|
||||||
|
this._evictAllPages();
|
||||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||||
this._pages.push(newPage);
|
this._pages.push(newPage);
|
||||||
this._activePages.push(newPage);
|
this._activePages.push(newPage);
|
||||||
@@ -191,7 +194,7 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
return newPage;
|
return newPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a - b);
|
||||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||||
|
|
||||||
// Merge into the new page
|
// Merge into the new page
|
||||||
@@ -206,8 +209,8 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
// Add the new merged page to the end
|
// Add the new merged page to the end
|
||||||
this.pages.push(mergedPage);
|
this.pages.push(mergedPage);
|
||||||
|
|
||||||
// Request the model to be cleared to refresh all texture pages.
|
// Invalidate renderer models so all texture pages are refreshed.
|
||||||
this._requestClearModel = true;
|
this._pageLayoutVersion++;
|
||||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,6 +261,23 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _evictAllPages(): void {
|
||||||
|
const startTime = performance.now();
|
||||||
|
const pageCount = this._pages.length;
|
||||||
|
for (const page of this._pages) {
|
||||||
|
this._onRemoveTextureAtlasCanvas.fire(page.canvas);
|
||||||
|
page.canvas.remove();
|
||||||
|
}
|
||||||
|
this._pages.length = 0;
|
||||||
|
this._activePages.length = 0;
|
||||||
|
this._overflowSizePage = undefined;
|
||||||
|
this._cacheMap.clear();
|
||||||
|
this._cacheMapCombined.clear();
|
||||||
|
this._didWarmUp = false;
|
||||||
|
this._pageLayoutVersion++;
|
||||||
|
this._logService.debug(`Evicted ${pageCount} WebGL atlas pages in ${(performance.now() - startTime).toFixed(2)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
|
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||||
}
|
}
|
||||||
@@ -815,11 +835,15 @@ export class TextureAtlas implements ITextureAtlas {
|
|||||||
// Create a new page for oversized glyphs as they come up
|
// Create a new page for oversized glyphs as they come up
|
||||||
if (rasterizedGlyph.size.x > this._textureSize) {
|
if (rasterizedGlyph.size.x > this._textureSize) {
|
||||||
if (!this._overflowSizePage) {
|
if (!this._overflowSizePage) {
|
||||||
|
// Make room for the oversized page without exceeding texture capacity.
|
||||||
|
if (TextureAtlas.maxAtlasPages && this._pages.length >= TextureAtlas.maxAtlasPages) {
|
||||||
|
this._evictAllPages();
|
||||||
|
}
|
||||||
this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize);
|
this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize);
|
||||||
this.pages.push(this._overflowSizePage);
|
this.pages.push(this._overflowSizePage);
|
||||||
|
|
||||||
// Request the model to be cleared to refresh all texture pages.
|
// Invalidate renderer models so all texture pages are refreshed.
|
||||||
this._requestClearModel = true;
|
this._pageLayoutVersion++;
|
||||||
this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas);
|
this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas);
|
||||||
}
|
}
|
||||||
activePage = this._overflowSizePage;
|
activePage = this._overflowSizePage;
|
||||||
@@ -1080,17 +1104,33 @@ class AtlasPage {
|
|||||||
size: number,
|
size: number,
|
||||||
sourcePages?: AtlasPage[]
|
sourcePages?: AtlasPage[]
|
||||||
) {
|
) {
|
||||||
if (sourcePages) {
|
|
||||||
for (const p of sourcePages) {
|
|
||||||
this._glyphs.push(...p.glyphs);
|
|
||||||
this._usedPixels += p._usedPixels;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.canvas = createCanvas(document, size, size);
|
this.canvas = createCanvas(document, size, size);
|
||||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||||
// set.
|
// set.
|
||||||
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
|
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
|
||||||
|
if (sourcePages) {
|
||||||
|
if (sourcePages.length === 4) {
|
||||||
|
// optimized for quadmerge
|
||||||
|
this._glyphs = this._glyphs.concat(
|
||||||
|
sourcePages[0].glyphs,
|
||||||
|
sourcePages[1].glyphs,
|
||||||
|
sourcePages[2].glyphs,
|
||||||
|
sourcePages[3].glyphs
|
||||||
|
);
|
||||||
|
this._usedPixels = sourcePages[0]._usedPixels +
|
||||||
|
sourcePages[1]._usedPixels +
|
||||||
|
sourcePages[2]._usedPixels +
|
||||||
|
sourcePages[3]._usedPixels;
|
||||||
|
} else {
|
||||||
|
// fallback for non quadmerges (should never be used)
|
||||||
|
for (let i = 0; i < sourcePages.length; ++i) {
|
||||||
|
this._glyphs = this._glyphs.concat(sourcePages[i].glyphs);
|
||||||
|
this._usedPixels += sourcePages[i]._usedPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
@@ -1125,7 +1165,7 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThreshol
|
|||||||
// were covered (fg=#8ae234, bg=#c4a000).
|
// were covered (fg=#8ae234, bg=#c4a000).
|
||||||
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
|
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
|
||||||
|
|
||||||
// Set alpha channel of relevent pixels to 0
|
// Set alpha channel of relevant pixels to 0
|
||||||
let isEmpty = true;
|
let isEmpty = true;
|
||||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||||
// Check exact match
|
// Check exact match
|
||||||
|
|||||||
@@ -68,10 +68,12 @@ export interface ITextureAtlas extends IDisposable {
|
|||||||
warmUp(): void;
|
warmUp(): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
|
* Incremented whenever cached glyph texture page mappings may be stale, such as after atlas page
|
||||||
* for a new set of glyphs.
|
* merges or overflow page creation. Renderers compare this against their own last-seen value and
|
||||||
|
* rebuild their model when it changes; a shared atlas can have many renderers, so this must not
|
||||||
|
* be a consume-once flag.
|
||||||
*/
|
*/
|
||||||
beginFrame(): boolean;
|
readonly pageLayoutVersion: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear all glyphs from the texture atlas.
|
* Clear all glyphs from the texture atlas.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
|||||||
};
|
};
|
||||||
const gl = document.createElement('canvas').getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
const gl = document.createElement('canvas').getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
||||||
if (!gl) {
|
if (!gl) {
|
||||||
throw new Error('Webgl2 is only supported on Safari 16 and above');
|
throw new Error('WebGL2 is only supported on Safari 16 and above');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super();
|
super();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { CursorBlinkStateManager } from './CursorBlinkStateManager';
|
|||||||
import { observeDevicePixelDimensions } from './DevicePixelObserver';
|
import { observeDevicePixelDimensions } from './DevicePixelObserver';
|
||||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
import { CharData, IBufferLine, ICellData } from 'common/buffer/Types';
|
||||||
import { AttributeData } from 'common/buffer/AttributeData';
|
import { AttributeData } from 'common/buffer/AttributeData';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
import { CellData } from 'common/buffer/CellData';
|
||||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||||
@@ -456,7 +456,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
for (y = start; y <= end; y++) {
|
for (y = start; y <= end; y++) {
|
||||||
row = y + terminal.buffer.ydisp;
|
row = y + terminal.buffer.ydisp;
|
||||||
line = terminal.buffer.lines.get(row)!;
|
const bufferLine = terminal.buffer.lines.get(row);
|
||||||
|
if (!bufferLine) {
|
||||||
|
this._model.lineLengths[y] = 0;
|
||||||
|
for (x = 0; x < terminal.cols; x++) {
|
||||||
|
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||||
|
modelUpdated = true;
|
||||||
|
this._nullModelCell(x, y, j, 0, 0, 0);
|
||||||
|
}
|
||||||
|
this._setRowBlinkState(y, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
line = bufferLine;
|
||||||
let rowHasBlinkingCells = false;
|
let rowHasBlinkingCells = false;
|
||||||
this._model.lineLengths[y] = 0;
|
this._model.lineLengths[y] = 0;
|
||||||
isCursorRow = cursorY === row;
|
isCursorRow = cursorY === row;
|
||||||
@@ -587,13 +598,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// Null out non-first cells
|
// Null out non-first cells
|
||||||
for (x++; x <= lastCharX; x++) {
|
for (x++; x <= lastCharX; x++) {
|
||||||
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||||
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
|
|
||||||
this._model.cells[j] = NULL_CELL_CODE;
|
|
||||||
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
|
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
|
||||||
// supported
|
// supported
|
||||||
this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
|
this._nullModelCell(x, y, j, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||||
this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
|
|
||||||
this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
|
|
||||||
}
|
}
|
||||||
x--; // Go back to the previous update cell for next iteration
|
x--; // Go back to the previous update cell for next iteration
|
||||||
}
|
}
|
||||||
@@ -607,6 +614,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._updateTextBlinkState();
|
this._updateTextBlinkState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _nullModelCell(x: number, y: number, cellIndex: number, bg: number, fg: number, ext: number): void {
|
||||||
|
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, bg, fg, ext, NULL_CELL_CHAR, 0, 0);
|
||||||
|
this._model.cells[cellIndex] = NULL_CELL_CODE;
|
||||||
|
this._model.cells[cellIndex + RenderModelConstants.BG_OFFSET] = bg;
|
||||||
|
this._model.cells[cellIndex + RenderModelConstants.FG_OFFSET] = fg;
|
||||||
|
this._model.cells[cellIndex + RenderModelConstants.EXT_OFFSET] = ext;
|
||||||
|
}
|
||||||
|
|
||||||
private _resetBlinkingRowState(): void {
|
private _resetBlinkingRowState(): void {
|
||||||
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
|
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
|
||||||
this._rowHasBlinkingCellsCount = 0;
|
this._rowHasBlinkingCellsCount = 0;
|
||||||
@@ -664,10 +679,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
||||||
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
||||||
|
|
||||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
// The size of the canvas on the page. It's important that this rounds to nearest integer
|
||||||
// and not ceils as browsers often have floating point precision issues where
|
// and not ceils as browsers often have floating point precision issues where
|
||||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
// it's actually 1.1. Ceiling may cause blurriness as the backing canvas image is 1 pixel too
|
||||||
// large for the canvas element size.
|
// large for the canvas element size.
|
||||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
||||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { createPatternCanvas } from './CustomGlyphRasterizer';
|
||||||
|
|
||||||
|
describe('CustomGlyphRasterizer', () => {
|
||||||
|
describe('createPatternCanvas', () => {
|
||||||
|
it('prefers an offscreen canvas without using the DOM canvas factory', () => {
|
||||||
|
const expectedCanvas = {} as OffscreenCanvas;
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const canvas = createPatternCanvas(
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
(width, height) => {
|
||||||
|
calls.push(`offscreen:${width}x${height}`);
|
||||||
|
return expectedCanvas;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
calls.push('dom');
|
||||||
|
return {} as HTMLCanvasElement;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual({ canvas, calls }, {
|
||||||
|
canvas: expectedCanvas,
|
||||||
|
calls: ['offscreen:2x3']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the main-realm DOM canvas factory when OffscreenCanvas is unavailable', () => {
|
||||||
|
const expectedCanvas = {} as HTMLCanvasElement;
|
||||||
|
const calls: string[] = [];
|
||||||
|
|
||||||
|
const canvas = createPatternCanvas(
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
undefined,
|
||||||
|
(width, height) => {
|
||||||
|
calls.push(`dom:${width}x${height}`);
|
||||||
|
return expectedCanvas;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual({ canvas, calls }, {
|
||||||
|
canvas: expectedCanvas,
|
||||||
|
calls: ['dom:4x5']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,6 +8,29 @@ import type { ILogService } from 'common/services/Services';
|
|||||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
||||||
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||||
|
|
||||||
|
type PatternCanvas = HTMLCanvasElement | OffscreenCanvas;
|
||||||
|
type PatternCanvasFactory = (width: number, height: number) => PatternCanvas;
|
||||||
|
|
||||||
|
const createOffscreenPatternCanvas: PatternCanvasFactory | undefined = typeof OffscreenCanvas === 'undefined'
|
||||||
|
? undefined
|
||||||
|
: (width, height) => new OffscreenCanvas(width, height);
|
||||||
|
|
||||||
|
const createDomPatternCanvas: PatternCanvasFactory = (width, height) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createPatternCanvas(
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
offscreenCanvasFactory: PatternCanvasFactory | undefined = createOffscreenPatternCanvas,
|
||||||
|
domCanvasFactory: PatternCanvasFactory = createDomPatternCanvas
|
||||||
|
): PatternCanvas {
|
||||||
|
return offscreenCanvasFactory?.(width, height) ?? domCanvasFactory(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||||
* successfully drawn.
|
* successfully drawn.
|
||||||
@@ -221,8 +244,8 @@ function drawPathDefinitionCharacter(
|
|||||||
const rx = parseFloat(args[0]) * deviceCellWidth;
|
const rx = parseFloat(args[0]) * deviceCellWidth;
|
||||||
const ry = parseFloat(args[1]) * deviceCellHeight;
|
const ry = parseFloat(args[1]) * deviceCellHeight;
|
||||||
const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
|
const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
|
||||||
const largeArcFlag = parseInt(args[3]);
|
const largeArcFlag = parseInt(args[3], 10);
|
||||||
const sweepFlag = parseInt(args[4]);
|
const sweepFlag = parseInt(args[4], 10);
|
||||||
const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
|
const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
|
||||||
const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
|
const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
|
||||||
drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
|
drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
|
||||||
@@ -458,9 +481,9 @@ function drawPatternChar(
|
|||||||
if (!pattern) {
|
if (!pattern) {
|
||||||
const width = charDefinition[0].length;
|
const width = charDefinition[0].length;
|
||||||
const height = charDefinition.length;
|
const height = charDefinition.length;
|
||||||
const tmpCanvas = ctx.canvas.ownerDocument.createElement('canvas');
|
// The atlas canvas can be adopted into another document, so temporary resources must not use
|
||||||
tmpCanvas.width = width;
|
// its mutable ownerDocument.
|
||||||
tmpCanvas.height = height;
|
const tmpCanvas = createPatternCanvas(width, height);
|
||||||
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
|
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
|
||||||
const imageData = new ImageData(width, height);
|
const imageData = new ImageData(width, height);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,481 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IImage32, decodePng } from '@lunapaint/png-codec';
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
|
||||||
|
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
|
||||||
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||||
|
|
||||||
|
type CellSignature = number[];
|
||||||
|
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
|
||||||
|
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
|
||||||
|
|
||||||
|
interface ITestTextureAtlasConstructor {
|
||||||
|
maxAtlasPages: number | undefined;
|
||||||
|
maxTextureSize: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestAtlasPage {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestTextureAtlas {
|
||||||
|
constructor: ITestTextureAtlasConstructor;
|
||||||
|
pages: ITestAtlasPage[];
|
||||||
|
pageLayoutVersion: number;
|
||||||
|
_overflowSizePage?: ITestAtlasPage;
|
||||||
|
_textureSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestGlyphRenderer {
|
||||||
|
_atlasTextures: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderer {
|
||||||
|
_charAtlas?: ITestTextureAtlas;
|
||||||
|
_glyphRenderer?: {
|
||||||
|
value?: ITestGlyphRenderer;
|
||||||
|
};
|
||||||
|
dimensions: {
|
||||||
|
device: {
|
||||||
|
cell: {
|
||||||
|
width: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderService {
|
||||||
|
_renderer?: {
|
||||||
|
value?: ITestRenderer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestTerminal extends Terminal {
|
||||||
|
_core?: {
|
||||||
|
_renderService?: ITestRenderService;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestWebglAddon extends WebglAddon {
|
||||||
|
_renderer?: ITestRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAtlasEventStats {
|
||||||
|
removals: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestWindow extends Window {
|
||||||
|
Terminal: TestTerminalConstructor;
|
||||||
|
WebglAddon: TestWebglAddonConstructor;
|
||||||
|
term: ITestTerminal;
|
||||||
|
termB?: ITestTerminal;
|
||||||
|
addon?: ITestWebglAddon;
|
||||||
|
addonB?: ITestWebglAddon;
|
||||||
|
atlasEventStats?: IAtlasEventStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAtlasLimits {
|
||||||
|
maxAtlasPages: number;
|
||||||
|
maxTextureSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAtlasState {
|
||||||
|
pages: number;
|
||||||
|
atlasTextures: number;
|
||||||
|
pageLayoutVersion: number;
|
||||||
|
overflowPageCreated: boolean;
|
||||||
|
removals: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NORMAL_HEADER = ' NORMAL_REF_0123456789';
|
||||||
|
const POST_EVICTION_TEXT = ' AFTER_EVICTION_0123456789';
|
||||||
|
const NORMAL_HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
const POST_EVICTION_COLS = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
||||||
|
const TERM_B_SELECTOR = '#terminal-container-b .xterm-screen';
|
||||||
|
const TERM_SELECTOR = '#terminal-container .xterm-screen';
|
||||||
|
|
||||||
|
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
w.addon = new w.WebglAddon({ preserveDrawingBuffer: true });
|
||||||
|
w.term.loadAddon(w.addon);
|
||||||
|
});
|
||||||
|
const isWebglRenderer = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
return !!w.addon && w.term._core?._renderService?._renderer?.value === w.addon._renderer;
|
||||||
|
});
|
||||||
|
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTerminalB(ctx: ITestContext, cols: number): Promise<void> {
|
||||||
|
await ctx.page.evaluate(columnCount => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
w.termB?.dispose();
|
||||||
|
document.getElementById('terminal-container-b')?.remove();
|
||||||
|
const element = document.createElement('div');
|
||||||
|
element.id = 'terminal-container-b';
|
||||||
|
document.body.appendChild(element);
|
||||||
|
w.termB = new w.Terminal({ cols: columnCount, rows: 24, allowProposedApi: true });
|
||||||
|
w.termB.open(element);
|
||||||
|
w.addonB = new w.WebglAddon({ preserveDrawingBuffer: true });
|
||||||
|
w.termB.loadAddon(w.addonB);
|
||||||
|
}, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeToBAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
await ctx.page.evaluate(value => new Promise<void>(resolve => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
if (!w.termB) {
|
||||||
|
throw new Error('Terminal B must be created before writing to it');
|
||||||
|
}
|
||||||
|
const disposable = w.termB.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
w.termB.write(value);
|
||||||
|
}), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
const renderPromise = new Promise<void>(resolve => {
|
||||||
|
const disposable = ctx.proxy.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await ctx.proxy.write(data);
|
||||||
|
await renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAndWaitForRender(ctx: ITestContext, terminal: 'term' | 'termB'): Promise<void> {
|
||||||
|
await ctx.page.evaluate(name => new Promise<void>(resolve => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const term = w[name];
|
||||||
|
if (!term) {
|
||||||
|
throw new Error(`${name} must exist before refreshing it`);
|
||||||
|
}
|
||||||
|
const disposable = term.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
term.refresh(0, term.rows - 1);
|
||||||
|
}), terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function configureAtlasLimits(ctx: ITestContext): Promise<IAtlasLimits> {
|
||||||
|
const limits = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (!atlas || atlas.constructor.maxAtlasPages === undefined || atlas.constructor.maxTextureSize === undefined) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const result = {
|
||||||
|
maxAtlasPages: atlas.constructor.maxAtlasPages,
|
||||||
|
maxTextureSize: atlas.constructor.maxTextureSize
|
||||||
|
};
|
||||||
|
atlas.constructor.maxAtlasPages = 4;
|
||||||
|
atlas.constructor.maxTextureSize = 512;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
expect(limits, 'TextureAtlas limits must be initialized').toBeDefined();
|
||||||
|
return limits!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreAtlasLimits(ctx: ITestContext, limits: IAtlasLimits | undefined): Promise<void> {
|
||||||
|
if (!limits) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await ctx.page.evaluate(original => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (atlas) {
|
||||||
|
atlas.constructor.maxAtlasPages = original.maxAtlasPages;
|
||||||
|
atlas.constructor.maxTextureSize = original.maxTextureSize;
|
||||||
|
}
|
||||||
|
}, limits);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installAtlasEventStats(ctx: ITestContext): Promise<void> {
|
||||||
|
const installed = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (!atlas || !w.addon) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stats: IAtlasEventStats = {
|
||||||
|
removals: 0,
|
||||||
|
maxPages: atlas.pages.length
|
||||||
|
};
|
||||||
|
w.atlasEventStats = stats;
|
||||||
|
w.addon.onAddTextureAtlasCanvas(() => {
|
||||||
|
stats.maxPages = Math.max(stats.maxPages, atlas.pages.length);
|
||||||
|
});
|
||||||
|
w.addon.onRemoveTextureAtlasCanvas(() => stats.removals++);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
expect(installed, 'atlas event tracking must be installed').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAtlasState(ctx: ITestContext): Promise<IAtlasState> {
|
||||||
|
const state = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const renderer = w.term._core?._renderService?._renderer?.value;
|
||||||
|
const atlas = renderer?._charAtlas;
|
||||||
|
const glyphRenderer = renderer?._glyphRenderer?.value;
|
||||||
|
const stats = w.atlasEventStats;
|
||||||
|
if (!atlas || !glyphRenderer || !stats) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
pages: atlas.pages.length,
|
||||||
|
atlasTextures: glyphRenderer._atlasTextures.length,
|
||||||
|
pageLayoutVersion: atlas.pageLayoutVersion,
|
||||||
|
overflowPageCreated: !!atlas._overflowSizePage,
|
||||||
|
removals: stats.removals,
|
||||||
|
maxPages: stats.maxPages
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(state, 'atlas state must be available').toBeDefined();
|
||||||
|
return state!;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUniqueGlyphFlood(count: number, cols: number, offset: number): string {
|
||||||
|
const base = 0x4E00;
|
||||||
|
const range = 0x9FFF - base;
|
||||||
|
const perRow = Math.max(1, Math.floor(cols / 2));
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result += String.fromCodePoint(base + ((offset + i) % range));
|
||||||
|
if ((i + 1) % perRow === 0 && i + 1 < count) {
|
||||||
|
result += '\r\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureRowSignatures(ctx: ITestContext, selector: string, cols: number, row: number, sampleCols: number[], debugName: string): Promise<CellSignature[]> {
|
||||||
|
const screenshotOptions = process.env.DEBUG ? { path: `out-esbuild-test/playwright/${debugName}.png` } : undefined;
|
||||||
|
const buffer = await ctx.page.locator(selector).screenshot(screenshotOptions);
|
||||||
|
const frame = {
|
||||||
|
cols,
|
||||||
|
rows: 24,
|
||||||
|
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
|
||||||
|
};
|
||||||
|
return sampleCols.map(col => cellSignature(frame, col, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
|
||||||
|
const grid = 4;
|
||||||
|
const data = frame.decoded.data;
|
||||||
|
const imageWidth = frame.decoded.width;
|
||||||
|
const cellWidth = frame.decoded.width / frame.cols;
|
||||||
|
const cellHeight = frame.decoded.height / frame.rows;
|
||||||
|
const x0 = (col - 1) * cellWidth;
|
||||||
|
const y0 = (row - 1) * cellHeight;
|
||||||
|
const sums = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
const counts = new Array<number>(grid * grid).fill(0);
|
||||||
|
const startX = Math.max(0, Math.floor(x0));
|
||||||
|
const startY = Math.max(0, Math.floor(y0));
|
||||||
|
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
|
||||||
|
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
|
||||||
|
for (let x = startX; x < endX; x++) {
|
||||||
|
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
|
||||||
|
const sub = gy * grid + gx;
|
||||||
|
const i = (y * imageWidth + x) * 4;
|
||||||
|
sums[sub * 4] += data[i];
|
||||||
|
sums[sub * 4 + 1] += data[i + 1];
|
||||||
|
sums[sub * 4 + 2] += data[i + 2];
|
||||||
|
sums[sub * 4 + 3] += data[i + 3];
|
||||||
|
counts[sub]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
for (let sub = 0; sub < counts.length; sub++) {
|
||||||
|
if (counts[sub] > 0) {
|
||||||
|
signature[sub * 4] = sums[sub * 4] / counts[sub];
|
||||||
|
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
|
||||||
|
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
|
||||||
|
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function signatureDiff(actual: CellSignature, reference: CellSignature): number {
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < actual.length; i++) {
|
||||||
|
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
|
||||||
|
}
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
|
||||||
|
const diff = signatureDiff(actual, reference);
|
||||||
|
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('atlas page overflow (#6038)', () => {
|
||||||
|
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||||
|
test.describe.configure({ timeout: 90000 });
|
||||||
|
|
||||||
|
test('evicts normal pages when the page cap cannot be reduced by merging', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
const errors: string[] = [];
|
||||||
|
const onError = (error: Error): void => { errors.push(error.message); };
|
||||||
|
let limits: IAtlasLimits | undefined;
|
||||||
|
ctx.page.on('pageerror', onError);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
limits = await configureAtlasLimits(ctx);
|
||||||
|
|
||||||
|
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
await createTerminalB(ctx, 80);
|
||||||
|
await installAtlasEventStats(ctx);
|
||||||
|
|
||||||
|
const atlasShared = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
return w.term._core?._renderService?._renderer?.value?._charAtlas === w.termB?._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
});
|
||||||
|
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
|
||||||
|
|
||||||
|
await writeToBAndWaitForRender(ctx, `\x1b[H${NORMAL_HEADER}\x1b[2;1H${POST_EVICTION_TEXT}`);
|
||||||
|
const headerReference = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 1, NORMAL_HEADER_COLS, 'atlas-overflow-header-reference');
|
||||||
|
const postReference = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 2, POST_EVICTION_COLS, 'atlas-overflow-post-reference');
|
||||||
|
await writeToBAndWaitForRender(ctx, '\x1b[2;1H\x1b[2K');
|
||||||
|
|
||||||
|
const initialState = await getAtlasState(ctx);
|
||||||
|
const glyphsPerChunk = 23 * 40 - 1;
|
||||||
|
for (let chunk = 0; chunk < 24; chunk++) {
|
||||||
|
await ctx.proxy.write('\x1b[H\x1b[2J' + generateUniqueGlyphFlood(glyphsPerChunk, 80, chunk * glyphsPerChunk));
|
||||||
|
await ctx.page.waitForTimeout(50);
|
||||||
|
const state = await getAtlasState(ctx);
|
||||||
|
if (state.removals > 0 || state.pages > state.atlasTextures || errors.length > 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const evictionState = await getAtlasState(ctx);
|
||||||
|
expect(evictionState.removals, 'normal atlas pages must be evicted at the page cap').toBeGreaterThan(0);
|
||||||
|
expect(evictionState.pageLayoutVersion, 'eviction must invalidate renderer models').toBeGreaterThan(initialState.pageLayoutVersion);
|
||||||
|
expect(evictionState.maxPages, 'atlas pages must stay within renderer texture capacity').toBeLessThanOrEqual(evictionState.atlasTextures);
|
||||||
|
expect(errors, `renderer must not throw during atlas eviction: ${errors[0] ?? ''}`).toEqual([]);
|
||||||
|
|
||||||
|
await writeToBAndWaitForRender(ctx, `\x1b[2;1H${POST_EVICTION_TEXT}`);
|
||||||
|
const termBHeader = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
return w.termB?.buffer.active.getLine(0)?.translateToString(true);
|
||||||
|
});
|
||||||
|
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(NORMAL_HEADER);
|
||||||
|
|
||||||
|
const headerAfter = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 1, NORMAL_HEADER_COLS, 'atlas-overflow-header-after');
|
||||||
|
const postAfter = await captureRowSignatures(ctx, TERM_B_SELECTOR, 80, 2, POST_EVICTION_COLS, 'atlas-overflow-post-after');
|
||||||
|
for (let i = 0; i < NORMAL_HEADER_COLS.length; i++) {
|
||||||
|
expectSignatureMatches(headerAfter[i], headerReference[i], `existing header col ${NORMAL_HEADER_COLS[i]} changed after eviction`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < POST_EVICTION_COLS.length; i++) {
|
||||||
|
expectSignatureMatches(postAfter[i], postReference[i], `new text col ${POST_EVICTION_COLS[i]} changed after eviction`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stableState = await getAtlasState(ctx);
|
||||||
|
await refreshAndWaitForRender(ctx, 'termB');
|
||||||
|
const refreshedState = await getAtlasState(ctx);
|
||||||
|
expect(refreshedState.pageLayoutVersion, 'an unchanged full refresh must not evict again').toBe(stableState.pageLayoutVersion);
|
||||||
|
expect(refreshedState.removals, 'an unchanged full refresh must not remove more pages').toBe(stableState.removals);
|
||||||
|
} finally {
|
||||||
|
ctx.page.off('pageerror', onError);
|
||||||
|
await restoreAtlasLimits(ctx, limits).catch(() => {});
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('evicts before adding an oversized glyph page at the page cap', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
const errors: string[] = [];
|
||||||
|
const onError = (error: Error): void => { errors.push(error.message); };
|
||||||
|
let limits: IAtlasLimits | undefined;
|
||||||
|
ctx.page.on('pageerror', onError);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx, { cols: 160, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
limits = await configureAtlasLimits(ctx);
|
||||||
|
|
||||||
|
await openTerminal(ctx, { cols: 160, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
await installAtlasEventStats(ctx);
|
||||||
|
|
||||||
|
let state = await getAtlasState(ctx);
|
||||||
|
let offset = 0;
|
||||||
|
for (let chunk = 0; chunk < 120 && state.pages < state.atlasTextures; chunk++) {
|
||||||
|
const glyphCount = state.pages < state.atlasTextures - 1 ? 200 : 25;
|
||||||
|
await ctx.proxy.write('\x1b[H\x1b[2J' + generateUniqueGlyphFlood(glyphCount, 160, offset));
|
||||||
|
await ctx.page.waitForTimeout(50);
|
||||||
|
offset += glyphCount;
|
||||||
|
state = await getAtlasState(ctx);
|
||||||
|
}
|
||||||
|
expect(state.pages, 'normal atlas pages must reach the texture capacity before the oversized glyph').toBe(state.atlasTextures);
|
||||||
|
expect(state.overflowPageCreated, 'normal glyph filling must not create the oversized page').toBe(false);
|
||||||
|
expect(errors, `renderer must not fail while filling normal pages: ${errors[0] ?? ''}`).toEqual([]);
|
||||||
|
|
||||||
|
await writeAndWaitForRender(ctx, '\x1b[H\x1b[2K\x1b[24;1H');
|
||||||
|
const sampleCols = [1, 8, 16, 24, 32];
|
||||||
|
const blankSignatures = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-blank');
|
||||||
|
const joinedLength = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const renderer = w.term._core?._renderService?._renderer?.value;
|
||||||
|
const atlas = renderer?._charAtlas;
|
||||||
|
if (!renderer || !atlas) {
|
||||||
|
throw new Error('renderer and atlas must exist');
|
||||||
|
}
|
||||||
|
const cellWidth = renderer.dimensions.device.cell.width;
|
||||||
|
const length = Math.min(w.term.cols - 1, Math.ceil(atlas._textureSize / cellWidth) + 32);
|
||||||
|
if (length * cellWidth <= atlas._textureSize) {
|
||||||
|
throw new Error('joined glyph must be wider than a normal atlas page');
|
||||||
|
}
|
||||||
|
const marker = 'W'.repeat(length);
|
||||||
|
w.term.registerCharacterJoiner(text => text.startsWith(marker) ? [[0, length]] : []);
|
||||||
|
return length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const beforeOversized = await getAtlasState(ctx);
|
||||||
|
await ctx.proxy.write(`\x1b[H${'W'.repeat(joinedLength)}`);
|
||||||
|
await ctx.page.waitForTimeout(50);
|
||||||
|
const oversizedState = await getAtlasState(ctx);
|
||||||
|
expect(oversizedState.overflowPageCreated, 'the joined glyph must use the oversized atlas page').toBe(true);
|
||||||
|
expect(oversizedState.removals, 'normal pages must be evicted before adding the oversized page').toBeGreaterThan(beforeOversized.removals);
|
||||||
|
expect(oversizedState.maxPages, 'oversized page creation must stay within renderer texture capacity').toBeLessThanOrEqual(oversizedState.atlasTextures);
|
||||||
|
expect(oversizedState.pageLayoutVersion, 'oversized eviction must invalidate renderer models').toBeGreaterThan(beforeOversized.pageLayoutVersion);
|
||||||
|
expect(errors, `renderer must not throw while adding the oversized page: ${errors[0] ?? ''}`).toEqual([]);
|
||||||
|
|
||||||
|
const wideSignatures = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-after');
|
||||||
|
for (let i = 0; i < sampleCols.length; i++) {
|
||||||
|
expect(signatureDiff(wideSignatures[i], blankSignatures[i]), `wide glyph col ${sampleCols[i]} must render visible pixels`).toBeGreaterThan(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stableState = await getAtlasState(ctx);
|
||||||
|
await refreshAndWaitForRender(ctx, 'term');
|
||||||
|
const refreshedState = await getAtlasState(ctx);
|
||||||
|
expect(refreshedState.pageLayoutVersion, 'oversized glyph refresh must settle without another eviction').toBe(stableState.pageLayoutVersion);
|
||||||
|
expect(refreshedState.removals, 'oversized glyph refresh must not remove more pages').toBe(stableState.removals);
|
||||||
|
const wideAfterRefresh = await captureRowSignatures(ctx, TERM_SELECTOR, 160, 1, sampleCols, 'atlas-overflow-wide-refreshed');
|
||||||
|
for (let i = 0; i < sampleCols.length; i++) {
|
||||||
|
expectSignatureMatches(wideAfterRefresh[i], wideSignatures[i], `wide glyph col ${sampleCols[i]} changed after refresh`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ctx.page.off('pageerror', onError);
|
||||||
|
await restoreAtlasLimits(ctx, limits).catch(() => {});
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { decodePng, type IImage32 } from '@lunapaint/png-codec';
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
|
||||||
|
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
|
||||||
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||||
|
|
||||||
|
type CellSignature = number[];
|
||||||
|
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
|
||||||
|
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
|
||||||
|
|
||||||
|
interface ITestTextureAtlas {
|
||||||
|
readonly pages: ReadonlyArray<{ readonly canvas: HTMLCanvasElement }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestGlyphRenderer {
|
||||||
|
readonly _atlasTextures: ReadonlyArray<unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderer {
|
||||||
|
_charAtlas?: ITestTextureAtlas;
|
||||||
|
_glyphRenderer?: {
|
||||||
|
value?: ITestGlyphRenderer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderService {
|
||||||
|
_renderer?: {
|
||||||
|
value?: ITestRenderer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestTerminal extends Terminal {
|
||||||
|
_core?: {
|
||||||
|
_renderService?: ITestRenderService;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestWebglAddon extends WebglAddon {
|
||||||
|
_renderer?: ITestRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAtlasEventStats {
|
||||||
|
additions: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestWindow {
|
||||||
|
WebglAddon: TestWebglAddonConstructor;
|
||||||
|
term: ITestTerminal;
|
||||||
|
addon?: ITestWebglAddon;
|
||||||
|
atlasEventStats?: IAtlasEventStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IAtlasState {
|
||||||
|
additions: number;
|
||||||
|
atlasTextures: number;
|
||||||
|
maxPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const REFERENCE_LINE = 'Ref0123456789ABCDEF';
|
||||||
|
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19];
|
||||||
|
const TERM_SELECTOR = '#terminal-container .xterm-screen';
|
||||||
|
|
||||||
|
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
w.addon = new w.WebglAddon({ preserveDrawingBuffer: true });
|
||||||
|
w.term.loadAddon(w.addon);
|
||||||
|
});
|
||||||
|
const isWebglRenderer = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
return !!w.addon && w.term._core?._renderService?._renderer?.value === w.addon._renderer;
|
||||||
|
});
|
||||||
|
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installAtlasEventStats(ctx: ITestContext): Promise<void> {
|
||||||
|
const installed = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const atlas = w.term._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (!atlas || !w.addon) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stats: IAtlasEventStats = {
|
||||||
|
additions: 0,
|
||||||
|
maxPages: atlas.pages.length
|
||||||
|
};
|
||||||
|
w.atlasEventStats = stats;
|
||||||
|
w.addon.onAddTextureAtlasCanvas(() => {
|
||||||
|
stats.additions++;
|
||||||
|
stats.maxPages = Math.max(stats.maxPages, atlas.pages.length);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
expect(installed, 'atlas event tracking must be installed').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAtlasState(ctx: ITestContext): Promise<IAtlasState> {
|
||||||
|
const state = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
const renderer = w.term._core?._renderService?._renderer?.value;
|
||||||
|
const glyphRenderer = renderer?._glyphRenderer?.value;
|
||||||
|
const stats = w.atlasEventStats;
|
||||||
|
if (!glyphRenderer || !stats) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
additions: stats.additions,
|
||||||
|
atlasTextures: glyphRenderer._atlasTextures.length,
|
||||||
|
maxPages: stats.maxPages
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(state, 'atlas state must be available').toBeDefined();
|
||||||
|
return state!;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
const renderPromise = new Promise<void>(resolve => {
|
||||||
|
const disposable = ctx.proxy.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await ctx.proxy.write(data);
|
||||||
|
await renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAndWaitForRender(ctx: ITestContext): Promise<void> {
|
||||||
|
const renderPromise = new Promise<void>(resolve => {
|
||||||
|
const disposable = ctx.proxy.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
||||||
|
await renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSyntheticTuiFrames(frameCount: number, cols: number, rows: number): string[] {
|
||||||
|
const spinner = ['|', '/', '-', '\\'];
|
||||||
|
const words = ['reading', 'rendering', 'atlas', 'terminal', 'buffer', 'tokens', 'diff', 'patch'];
|
||||||
|
const frames: string[] = [];
|
||||||
|
for (let frameIndex = 0; frameIndex < frameCount; frameIndex++) {
|
||||||
|
const lines = [
|
||||||
|
`+${'-'.repeat(cols - 2)}+`,
|
||||||
|
`|${(` ${spinner[frameIndex % spinner.length]} Copilot is working`).padEnd(cols - 2).slice(0, cols - 2)}|`,
|
||||||
|
`+${'-'.repeat(cols - 2)}+`
|
||||||
|
];
|
||||||
|
for (let row = 3; row < rows - 1; row++) {
|
||||||
|
const foreground = 31 + ((frameIndex + row) % 6);
|
||||||
|
const text = `${words[(frameIndex + row) % words.length]} ${frameIndex}:${row}`;
|
||||||
|
lines.push(`\x1b[${foreground}m${text}\x1b[0m`);
|
||||||
|
}
|
||||||
|
lines.push(`\x1b[2mtokens ${1000 + frameIndex * 37}\x1b[0m`);
|
||||||
|
frames.push(`\x1b[2J\x1b[H${lines.join('\r\n')}`);
|
||||||
|
}
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUniqueGlyphFlood(count: number, cols: number, offset: number): string {
|
||||||
|
const base = 0x4E00;
|
||||||
|
const range = 0x9FFF - base;
|
||||||
|
const perRow = Math.max(1, Math.floor(cols / 2));
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
result += String.fromCodePoint(base + ((offset + i) % range));
|
||||||
|
if ((i + 1) % perRow === 0 && i + 1 < count) {
|
||||||
|
result += '\r\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureRowSignatures(ctx: ITestContext, row: number, cols: number[], debugName: string): Promise<CellSignature[]> {
|
||||||
|
const screenshotOptions = process.env.DEBUG ? { path: `out-esbuild-test/playwright/${debugName}.png` } : undefined;
|
||||||
|
const buffer = await ctx.page.locator(TERM_SELECTOR).screenshot(screenshotOptions);
|
||||||
|
const frame = {
|
||||||
|
cols: await ctx.proxy.cols,
|
||||||
|
rows: await ctx.proxy.rows,
|
||||||
|
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
|
||||||
|
};
|
||||||
|
return cols.map(col => cellSignature(frame, col, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
|
||||||
|
const grid = 4;
|
||||||
|
const data = frame.decoded.data;
|
||||||
|
const imageWidth = frame.decoded.width;
|
||||||
|
const cellWidth = frame.decoded.width / frame.cols;
|
||||||
|
const cellHeight = frame.decoded.height / frame.rows;
|
||||||
|
const x0 = (col - 1) * cellWidth;
|
||||||
|
const y0 = (row - 1) * cellHeight;
|
||||||
|
const sums = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
const counts = new Array<number>(grid * grid).fill(0);
|
||||||
|
const startX = Math.max(0, Math.floor(x0));
|
||||||
|
const startY = Math.max(0, Math.floor(y0));
|
||||||
|
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
|
||||||
|
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
|
||||||
|
for (let x = startX; x < endX; x++) {
|
||||||
|
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
|
||||||
|
const sub = gy * grid + gx;
|
||||||
|
const i = (y * imageWidth + x) * 4;
|
||||||
|
sums[sub * 4] += data[i];
|
||||||
|
sums[sub * 4 + 1] += data[i + 1];
|
||||||
|
sums[sub * 4 + 2] += data[i + 2];
|
||||||
|
sums[sub * 4 + 3] += data[i + 3];
|
||||||
|
counts[sub]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
for (let sub = 0; sub < counts.length; sub++) {
|
||||||
|
if (counts[sub] > 0) {
|
||||||
|
signature[sub * 4] = sums[sub * 4] / counts[sub];
|
||||||
|
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
|
||||||
|
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
|
||||||
|
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < actual.length; i++) {
|
||||||
|
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
|
||||||
|
}
|
||||||
|
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('WebGL atlas TUI stress (#6038)', () => {
|
||||||
|
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||||
|
test.describe.configure({ timeout: 90000 });
|
||||||
|
|
||||||
|
test('primary-buffer glyphs survive alternate-buffer redraws and atlas page churn', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
const errors: string[] = [];
|
||||||
|
const onError = (error: Error): void => { errors.push(error.message); };
|
||||||
|
ctx.page.on('pageerror', onError);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
await installAtlasEventStats(ctx);
|
||||||
|
|
||||||
|
await writeAndWaitForRender(ctx, REFERENCE_LINE);
|
||||||
|
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS, 'atlas-stress-reference');
|
||||||
|
|
||||||
|
await writeAndWaitForRender(ctx, '\x1b[?1049h');
|
||||||
|
for (const frame of generateSyntheticTuiFrames(30, 80, 24)) {
|
||||||
|
await writeAndWaitForRender(ctx, frame);
|
||||||
|
}
|
||||||
|
const glyphsPerViewport = 23 * 40 - 1;
|
||||||
|
for (let chunk = 0; chunk < 4; chunk++) {
|
||||||
|
await writeAndWaitForRender(ctx, `\x1b[2J\x1b[H${generateUniqueGlyphFlood(glyphsPerViewport, 80, chunk * glyphsPerViewport)}`);
|
||||||
|
}
|
||||||
|
await writeAndWaitForRender(ctx, '\x1b[?1049l');
|
||||||
|
await refreshAndWaitForRender(ctx);
|
||||||
|
|
||||||
|
const state = await getAtlasState(ctx);
|
||||||
|
expect(state.additions, 'stress workload must add atlas pages').toBeGreaterThan(0);
|
||||||
|
expect(state.maxPages, 'stress workload must exercise multiple atlas pages').toBeGreaterThan(1);
|
||||||
|
expect(state.maxPages, 'atlas pages must remain within renderer texture capacity').toBeLessThanOrEqual(state.atlasTextures);
|
||||||
|
expect(errors, `renderer must not throw during TUI stress: ${errors[0] ?? ''}`).toEqual([]);
|
||||||
|
|
||||||
|
const primaryText = await ctx.page.evaluate(() => {
|
||||||
|
const w = window as unknown as ITestWindow;
|
||||||
|
return w.term.buffer.active.getLine(0)?.translateToString(true);
|
||||||
|
});
|
||||||
|
expect(primaryText, 'primary buffer must still contain the reference line').toBe(REFERENCE_LINE);
|
||||||
|
|
||||||
|
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS, 'atlas-stress-after');
|
||||||
|
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||||
|
expectSignatureMatches(after[i], reference[i], `primary-buffer glyph at col ${REFERENCE_COLS[i]} changed after TUI stress`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
ctx.page.off('pageerror', onError);
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import { platform } from 'os';
|
||||||
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||||
|
|
||||||
|
interface ITestRendererWithAtlasCanvas {
|
||||||
|
readonly _charAtlas?: {
|
||||||
|
readonly _tmpCanvas?: HTMLCanvasElement;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
const renderPromise = new Promise<void>(resolve => {
|
||||||
|
const disposable = ctx.proxy.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await ctx.proxy.write(data);
|
||||||
|
await renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('WebGL custom glyphs', () => {
|
||||||
|
if (platform() === 'linux') {
|
||||||
|
test.skip(({ browserName }) => browserName === 'firefox' || browserName === 'webkit');
|
||||||
|
}
|
||||||
|
|
||||||
|
test('pattern glyphs render after the terminal is adopted into another document', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
const errors: string[] = [];
|
||||||
|
const onError = (error: Error): void => { errors.push(error.message); };
|
||||||
|
ctx.page.on('pageerror', onError);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx);
|
||||||
|
await ctx.page.evaluate(`
|
||||||
|
window.addon = new window.WebglAddon({ customGlyphs: true });
|
||||||
|
window.term.loadAddon(window.addon);
|
||||||
|
`);
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const frame = document.createElement('iframe');
|
||||||
|
document.body.appendChild(frame);
|
||||||
|
const auxiliaryDocument = frame.contentDocument;
|
||||||
|
if (!auxiliaryDocument?.body || !window.term.element) {
|
||||||
|
throw new Error('Auxiliary document and terminal element must be available');
|
||||||
|
}
|
||||||
|
const renderer = window.term._core?._renderService?._renderer?.value as ITestRendererWithAtlasCanvas | undefined;
|
||||||
|
const atlasCanvas = renderer?._charAtlas?._tmpCanvas;
|
||||||
|
if (!atlasCanvas) {
|
||||||
|
throw new Error('Texture atlas canvas must be available');
|
||||||
|
}
|
||||||
|
// Model the atlas state after an uncached glyph has attached the measurement canvas.
|
||||||
|
window.term.element.appendChild(atlasCanvas);
|
||||||
|
auxiliaryDocument.body.appendChild(window.term.element);
|
||||||
|
if (atlasCanvas.ownerDocument !== auxiliaryDocument) {
|
||||||
|
throw new Error('Texture atlas canvas must be adopted into the auxiliary document');
|
||||||
|
}
|
||||||
|
auxiliaryDocument.createElement = () => {
|
||||||
|
throw new Error('Not allowed to create elements in the auxiliary document');
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
await writeAndWaitForRender(ctx, '\u2591');
|
||||||
|
await writeAndWaitForRender(ctx, 'X');
|
||||||
|
|
||||||
|
const line = await ctx.page.evaluate(() => window.term.buffer.active.getLine(0)?.translateToString(true));
|
||||||
|
expect(errors, `renderer must not create pattern canvases in the auxiliary document: ${errors[0] ?? ''}`).toEqual([]);
|
||||||
|
expect(line).toBe('\u2591X');
|
||||||
|
} finally {
|
||||||
|
ctx.page.off('pageerror', onError);
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IImage32, decodePng } from '@lunapaint/png-codec';
|
||||||
|
import test, { expect } from '@playwright/test';
|
||||||
|
import type { Terminal, ITerminalInitOnlyOptions, ITerminalOptions } from '@xterm/xterm';
|
||||||
|
import type { IWebglAddonOptions, WebglAddon } from '@xterm/addon-webgl';
|
||||||
|
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||||
|
|
||||||
|
type CellSignature = number[];
|
||||||
|
type TestTerminalConstructor = new (options?: ITerminalOptions & ITerminalInitOnlyOptions) => ITestTerminal;
|
||||||
|
type TestWebglAddonConstructor = new (options?: IWebglAddonOptions) => ITestWebglAddon;
|
||||||
|
|
||||||
|
interface ITestTextureAtlasConstructor {
|
||||||
|
maxAtlasPages: number | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestTextureAtlas {
|
||||||
|
constructor: ITestTextureAtlasConstructor;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderer {
|
||||||
|
_charAtlas?: ITestTextureAtlas;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestRenderService {
|
||||||
|
_renderer?: {
|
||||||
|
value?: ITestRenderer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestTerminal extends Terminal {
|
||||||
|
_core?: {
|
||||||
|
_renderService?: ITestRenderService;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ITestWebglAddon extends WebglAddon {
|
||||||
|
_renderer?: ITestRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window { // eslint-disable-line @typescript-eslint/naming-convention
|
||||||
|
Terminal: TestTerminalConstructor;
|
||||||
|
WebglAddon: TestWebglAddonConstructor;
|
||||||
|
term: ITestTerminal;
|
||||||
|
termB?: ITestTerminal;
|
||||||
|
addon?: ITestWebglAddon;
|
||||||
|
addonB?: ITestWebglAddon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEADER = ' HEADER_REF_0123456789';
|
||||||
|
const HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||||
|
const TERM_B_SELECTOR = '#terminal-container-b .xterm-screen';
|
||||||
|
|
||||||
|
async function loadWebglStrict(ctx: ITestContext): Promise<void> {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
window.addon = new window.WebglAddon({ preserveDrawingBuffer: true });
|
||||||
|
window.term.loadAddon(window.addon);
|
||||||
|
});
|
||||||
|
const isWebglRenderer = await ctx.page.evaluate(() => {
|
||||||
|
return !!window.addon && window.term?._core?._renderService?._renderer?.value === window.addon._renderer;
|
||||||
|
});
|
||||||
|
expect(isWebglRenderer, 'WebGL renderer must be active').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createTerminalB(ctx: ITestContext): Promise<void> {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
window.termB?.dispose();
|
||||||
|
document.getElementById('terminal-container-b')?.remove();
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.id = 'terminal-container-b';
|
||||||
|
document.body.appendChild(el);
|
||||||
|
window.termB = new window.Terminal({ cols: 80, rows: 24, allowProposedApi: true });
|
||||||
|
window.termB.open(el);
|
||||||
|
window.addonB = new window.WebglAddon({ preserveDrawingBuffer: true });
|
||||||
|
window.termB.loadAddon(window.addonB);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeToBAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
await ctx.page.evaluate(d => new Promise<void>(resolve => {
|
||||||
|
const termB = window.termB;
|
||||||
|
if (!termB) {
|
||||||
|
throw new Error('Terminal B must be created before writing to it');
|
||||||
|
}
|
||||||
|
const disposable = termB.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
termB.write(d);
|
||||||
|
}), data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||||
|
const renderPromise = new Promise<void>(resolve => {
|
||||||
|
const disposable = ctx.proxy.onRender(() => {
|
||||||
|
disposable.dispose();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await ctx.proxy.write(data);
|
||||||
|
await renderPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setMaxAtlasPages(ctx: ITestContext, maxAtlasPages: number): Promise<void> {
|
||||||
|
const applied = await ctx.page.evaluate(max => {
|
||||||
|
const atlas = window.term?._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (!atlas) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
atlas.constructor.maxAtlasPages = max;
|
||||||
|
return true;
|
||||||
|
}, maxAtlasPages);
|
||||||
|
expect(applied, 'should be able to set TextureAtlas.maxAtlasPages').toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetMaxAtlasPages(ctx: ITestContext): Promise<void> {
|
||||||
|
await ctx.page.evaluate(() => {
|
||||||
|
const atlas = window.term?._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
if (atlas) {
|
||||||
|
atlas.constructor.maxAtlasPages = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateColoredAsciiFlood(cells: number, offset: number = 0): string {
|
||||||
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
|
let out = '';
|
||||||
|
for (let i = 0; i < cells; i++) {
|
||||||
|
const ch = letters[(offset + i) % letters.length];
|
||||||
|
const fg = (offset + i) % 256;
|
||||||
|
const bg = (offset + i * 7) % 256;
|
||||||
|
out += `\x1b[38;5;${fg}m\x1b[48;5;${bg}m${ch}`;
|
||||||
|
if ((i + 1) % 78 === 0 && i + 1 < cells) {
|
||||||
|
out += '\x1b[0m\r\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out + '\x1b[0m';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function captureRowSignatures(ctx: ITestContext, row: number, cols: number[]): Promise<CellSignature[]> {
|
||||||
|
const screenshotOptions = process.env.DEBUG ? { path: 'out-esbuild-test/playwright/shared-atlas-term-b.png' } : undefined;
|
||||||
|
const buffer = await ctx.page.locator(TERM_B_SELECTOR).screenshot(screenshotOptions);
|
||||||
|
const frame = {
|
||||||
|
cols: 80,
|
||||||
|
rows: 24,
|
||||||
|
decoded: (await decodePng(new Uint8Array(buffer), { force32: true })).image
|
||||||
|
};
|
||||||
|
return cols.map(col => cellSignature(frame, col, row));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cellSignature(frame: { cols: number, rows: number, decoded: IImage32 }, col: number, row: number): CellSignature {
|
||||||
|
const grid = 4;
|
||||||
|
const data = frame.decoded.data;
|
||||||
|
const imageWidth = frame.decoded.width;
|
||||||
|
const cellWidth = frame.decoded.width / frame.cols;
|
||||||
|
const cellHeight = frame.decoded.height / frame.rows;
|
||||||
|
const x0 = (col - 1) * cellWidth;
|
||||||
|
const y0 = (row - 1) * cellHeight;
|
||||||
|
const sums = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
const counts = new Array<number>(grid * grid).fill(0);
|
||||||
|
const startX = Math.max(0, Math.floor(x0));
|
||||||
|
const startY = Math.max(0, Math.floor(y0));
|
||||||
|
const endX = Math.min(frame.decoded.width, Math.ceil(x0 + cellWidth));
|
||||||
|
const endY = Math.min(frame.decoded.height, Math.ceil(y0 + cellHeight));
|
||||||
|
|
||||||
|
for (let y = startY; y < endY; y++) {
|
||||||
|
const gy = Math.min(grid - 1, Math.max(0, Math.floor(((y - y0) / cellHeight) * grid)));
|
||||||
|
for (let x = startX; x < endX; x++) {
|
||||||
|
const gx = Math.min(grid - 1, Math.max(0, Math.floor(((x - x0) / cellWidth) * grid)));
|
||||||
|
const sub = gy * grid + gx;
|
||||||
|
const i = (y * imageWidth + x) * 4;
|
||||||
|
sums[sub * 4] += data[i];
|
||||||
|
sums[sub * 4 + 1] += data[i + 1];
|
||||||
|
sums[sub * 4 + 2] += data[i + 2];
|
||||||
|
sums[sub * 4 + 3] += data[i + 3];
|
||||||
|
counts[sub]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const signature = new Array<number>(grid * grid * 4).fill(0);
|
||||||
|
for (let sub = 0; sub < counts.length; sub++) {
|
||||||
|
if (counts[sub] > 0) {
|
||||||
|
signature[sub * 4] = sums[sub * 4] / counts[sub];
|
||||||
|
signature[sub * 4 + 1] = sums[sub * 4 + 1] / counts[sub];
|
||||||
|
signature[sub * 4 + 2] = sums[sub * 4 + 2] / counts[sub];
|
||||||
|
signature[sub * 4 + 3] = sums[sub * 4 + 3] / counts[sub];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function expectSignatureMatches(actual: CellSignature, reference: CellSignature, message: string): void {
|
||||||
|
let diff = 0;
|
||||||
|
for (let i = 0; i < actual.length; i++) {
|
||||||
|
diff = Math.max(diff, Math.abs(actual[i] - reference[i]));
|
||||||
|
}
|
||||||
|
expect(diff, `${message}; max channel diff ${diff}`).toBeLessThanOrEqual(14);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe('shared-atlas garble across terminals (#6038)', () => {
|
||||||
|
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||||
|
test.describe.configure({ timeout: 60000 });
|
||||||
|
|
||||||
|
test('keeps a second terminal rendering correctly after shared atlas page merges', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
await createTerminalB(ctx);
|
||||||
|
|
||||||
|
const atlasShared = await ctx.page.evaluate(() => {
|
||||||
|
return window.term._core?._renderService?._renderer?.value?._charAtlas === window.termB?._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
});
|
||||||
|
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
|
||||||
|
|
||||||
|
await setMaxAtlasPages(ctx, 4);
|
||||||
|
|
||||||
|
await writeToBAndWaitForRender(ctx, HEADER);
|
||||||
|
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||||
|
|
||||||
|
for (let c = 0; c < 10; c++) {
|
||||||
|
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78, c * 23 * 78));
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
|
||||||
|
const termBHeader = await ctx.page.evaluate(() => window.termB?.buffer.active.getLine(0)?.translateToString(true));
|
||||||
|
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(HEADER);
|
||||||
|
|
||||||
|
const after = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||||
|
for (let i = 0; i < HEADER_COLS.length; i++) {
|
||||||
|
expectSignatureMatches(after[i], reference[i], `terminal B header col ${HEADER_COLS[i]} garbled by terminal A's atlas merges`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await resetMaxAtlasPages(ctx).catch(() => {});
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keeps a second terminal rendering correctly after terminal A clears the shared atlas', async ({ browser }) => {
|
||||||
|
const ctx = await createTestContext(browser);
|
||||||
|
try {
|
||||||
|
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||||
|
await loadWebglStrict(ctx);
|
||||||
|
await createTerminalB(ctx);
|
||||||
|
|
||||||
|
const atlasShared = await ctx.page.evaluate(() => {
|
||||||
|
return window.term._core?._renderService?._renderer?.value?._charAtlas === window.termB?._core?._renderService?._renderer?.value?._charAtlas;
|
||||||
|
});
|
||||||
|
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
|
||||||
|
|
||||||
|
await writeToBAndWaitForRender(ctx, HEADER);
|
||||||
|
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||||
|
|
||||||
|
// Terminal A alone clears the shared atlas, as embedders do to force a redraw (VS Code calls
|
||||||
|
// this for every terminal on OS resume). This wipes the pages that terminal B's model still
|
||||||
|
// holds texture coords into.
|
||||||
|
await ctx.proxy.clearTextureAtlas();
|
||||||
|
|
||||||
|
// Terminal A repopulates the cleared pages with a different glyph set, so B's stale coords now
|
||||||
|
// resolve to whatever A rasterized into those slots.
|
||||||
|
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78));
|
||||||
|
|
||||||
|
// Nudge B into a render that does not touch row 1, mirroring a terminal that is idle but
|
||||||
|
// visible. Row 1 must still be drawn from a model B rebuilt against the current atlas.
|
||||||
|
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
|
||||||
|
const termBHeader = await ctx.page.evaluate(() => window.termB?.buffer.active.getLine(0)?.translateToString(true));
|
||||||
|
expect(termBHeader, 'terminal B buffer must still contain the header').toBe(HEADER);
|
||||||
|
|
||||||
|
const after = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||||
|
for (let i = 0; i < HEADER_COLS.length; i++) {
|
||||||
|
expectSignatureMatches(after[i], reference[i], `terminal B header col ${HEADER_COLS[i]} garbled by terminal A's atlas clear`);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await ctx.page.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,12 +23,12 @@ declare module '@xterm/addon-webgl' {
|
|||||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event that is fired when the a new page is added to the texture atlas.
|
* An event that is fired when a new page is added to the texture atlas.
|
||||||
*/
|
*/
|
||||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event that is fired when the a page is removed from the texture atlas.
|
* An event that is fired when a page is removed from the texture atlas.
|
||||||
*/
|
*/
|
||||||
public readonly onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
public readonly onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
# @license MIT
|
|
||||||
set -euo pipefail
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
||||||
cd "$ROOT"
|
|
||||||
|
|
||||||
if [[ ! -f src/common/parser/wasm/transition_table.h ]]; then
|
|
||||||
npm run build
|
|
||||||
node bin/gen_parser_transition_table.mjs
|
|
||||||
fi
|
|
||||||
|
|
||||||
WASM_DIR=src/common/parser/wasm
|
|
||||||
OUT="$WASM_DIR/parser.wasm"
|
|
||||||
|
|
||||||
# Apple Clang (Xcode CLT) has no wasm32 target; prefer Homebrew LLVM when needed.
|
|
||||||
wasm_clang_has_target() {
|
|
||||||
"$1" --print-targets 2>/dev/null | grep -q 'wasm32'
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_wasm_toolchain() {
|
|
||||||
local clang_bin=clang
|
|
||||||
if wasm_clang_has_target clang; then
|
|
||||||
:
|
|
||||||
else
|
|
||||||
local llvm_prefix=""
|
|
||||||
if command -v brew >/dev/null 2>&1; then
|
|
||||||
llvm_prefix="$(brew --prefix llvm 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
for prefix in "$llvm_prefix" /opt/homebrew/opt/llvm /usr/local/opt/llvm; do
|
|
||||||
if [[ -n "$prefix" && -x "$prefix/bin/clang" ]] && wasm_clang_has_target "$prefix/bin/clang"; then
|
|
||||||
clang_bin="$prefix/bin/clang"
|
|
||||||
local lld_prefix=""
|
|
||||||
if command -v brew >/dev/null 2>&1; then
|
|
||||||
lld_prefix="$(brew --prefix lld 2>/dev/null || true)"
|
|
||||||
fi
|
|
||||||
for lld_dir in "$lld_prefix/bin" /opt/homebrew/opt/lld/bin /usr/local/opt/lld/bin; do
|
|
||||||
if [[ -n "$lld_dir" && -x "$lld_dir/lld" ]]; then
|
|
||||||
PATH="$lld_dir:$(dirname "$clang_bin"):$PATH"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
PATH="$(dirname "$clang_bin"):$PATH"
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
if ! wasm_clang_has_target "$clang_bin"; then
|
|
||||||
echo "error: no clang with wasm32 support found (macOS: brew install llvm lld)" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
WASM_CLANG="$clang_bin"
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve_wasm_toolchain
|
|
||||||
|
|
||||||
"$WASM_CLANG" --target=wasm32-unknown-unknown -O2 -nostdlib -fuse-ld=lld \
|
|
||||||
-Wl,--no-entry \
|
|
||||||
-Wl,--export-memory \
|
|
||||||
-Wl,--export=reset \
|
|
||||||
-Wl,--export=scan \
|
|
||||||
-Wl,--export=probe_action \
|
|
||||||
-Wl,--export=set_state \
|
|
||||||
-Wl,--export=sync_params_from \
|
|
||||||
-Wl,--export=export_params_to \
|
|
||||||
-Wl,--export=get_params_len \
|
|
||||||
-Wl,--export=get_subparams_len \
|
|
||||||
-Wl,--export=get_subparams_ptr \
|
|
||||||
-Wl,--export=get_subparams_idx_ptr \
|
|
||||||
-Wl,--export=get_input_ptr \
|
|
||||||
-Wl,--export=get_kinds_ptr \
|
|
||||||
-Wl,--export=get_starts_ptr \
|
|
||||||
-Wl,--export=get_lengths_ptr \
|
|
||||||
-Wl,--export=get_aux_ptr \
|
|
||||||
-Wl,--export=get_param_starts_ptr \
|
|
||||||
-Wl,--export=get_param_counts_ptr \
|
|
||||||
-Wl,--export=get_params_ptr \
|
|
||||||
-Wl,--export=get_state_ptr \
|
|
||||||
-Wl,--export=get_header_ptr \
|
|
||||||
-Wl,--initial-memory=67108864 \
|
|
||||||
-Wl,--max-memory=67108864 \
|
|
||||||
"$WASM_DIR/parser.c" -o "$OUT"
|
|
||||||
|
|
||||||
node bin/embed_parser_wasm.mjs
|
|
||||||
|
|
||||||
echo "Built $OUT"
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { readFileSync, writeFileSync } from 'fs';
|
|
||||||
import { join, dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const root = join(__dirname, '..');
|
|
||||||
const wasm = readFileSync(join(root, 'src/common/parser/wasm/parser.wasm'));
|
|
||||||
const b64 = wasm.toString('base64');
|
|
||||||
|
|
||||||
const out = `/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
/** Base64-encoded parser.wasm (generated by bin/build_parser_wasm.sh) */
|
|
||||||
export const PARSER_WASM_BASE64 = '${b64}';\n`;
|
|
||||||
|
|
||||||
writeFileSync(join(root, 'src/common/parser/wasm/parser.wasm.embed.ts'), out);
|
|
||||||
console.info(`Embedded parser.wasm (${wasm.length} bytes)`);
|
|
||||||
@@ -19,14 +19,17 @@ for (const addon of addons) {
|
|||||||
// Demo job - This requires the others to be built so it's not included when building all
|
// Demo job - This requires the others to be built so it's not included when building all
|
||||||
// jobs.push(createJob('demo-client', [`--demo-client`]));
|
// jobs.push(createJob('demo-client', [`--demo-client`]));
|
||||||
|
|
||||||
await Promise.all(jobs.map((job, i) => {
|
const codes = await Promise.all(jobs.map(job => {
|
||||||
return new Promise(r => {
|
return new Promise(r => {
|
||||||
job.cp.on('exit', code => {
|
job.cp.on('exit', code => {
|
||||||
log(`Finished \x1b[32m${job.name}\x1b[0m${code ? ' \x1b[31mwith errors\x1b[0m' : ''}`);
|
log(`Finished \x1b[32m${job.name}\x1b[0m${code ? ' \x1b[31mwith errors\x1b[0m' : ''}`);
|
||||||
r(code);
|
r(code ?? 1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
if (codes.some(code => code !== 0)) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} message
|
* @param {string} message
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
// @ts-check
|
|
||||||
import { writeFileSync } from 'fs';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname, join } from 'path';
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
const root = join(__dirname, '..');
|
|
||||||
|
|
||||||
// Import compiled transition table
|
|
||||||
const { VT500_TRANSITION_TABLE } = await import(join(root, 'out/common/parser/EscapeSequenceParser.js'));
|
|
||||||
|
|
||||||
const table = VT500_TRANSITION_TABLE.table;
|
|
||||||
let out = '/* Auto-generated by bin/gen_parser_transition_table.mjs - do not edit */\n';
|
|
||||||
out += `#ifndef PARSER_TRANSITION_TABLE_H\n#define PARSER_TRANSITION_TABLE_H\n\n`;
|
|
||||||
out += `#include <stdint.h>\n\n`;
|
|
||||||
out += `#define PARSER_TABLE_SIZE ${table.length}\n\n`;
|
|
||||||
out += `static const uint16_t PARSER_TRANSITION_TABLE[PARSER_TABLE_SIZE] = {\n`;
|
|
||||||
for (let i = 0; i < table.length; i++) {
|
|
||||||
if (i % 16 === 0) out += ' ';
|
|
||||||
out += `0x${table[i].toString(16)}`;
|
|
||||||
out += i < table.length - 1 ? ', ' : '';
|
|
||||||
if (i % 16 === 15) out += '\n';
|
|
||||||
}
|
|
||||||
if (table.length % 16 !== 0) out += '\n';
|
|
||||||
out += `};\n\n#endif\n`;
|
|
||||||
|
|
||||||
const headerPath = join(root, 'src/common/parser/wasm/transition_table.h');
|
|
||||||
writeFileSync(headerPath, out);
|
|
||||||
console.info(`Wrote ${headerPath} (${table.length} entries)`);
|
|
||||||
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
|||||||
import { basename, dirname, resolve, sep } from 'node:path';
|
import { basename, dirname, resolve, sep } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
||||||
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|||||||
|
|
||||||
/** @param {string} message */
|
/** @param {string} message */
|
||||||
function log(message) {
|
function log(message) {
|
||||||
console.info(`[setup-fast] ${message}`);
|
console.info(`[setup-full] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @param {string[]} args */
|
/** @param {string[]} args */
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ let argv = process.argv.slice(2);
|
|||||||
let suiteFilter = undefined;
|
let suiteFilter = undefined;
|
||||||
while (argv.some(e => e.startsWith('--suite='))) {
|
while (argv.some(e => e.startsWith('--suite='))) {
|
||||||
const i = argv.findIndex(e => e.startsWith('--suite='));
|
const i = argv.findIndex(e => e.startsWith('--suite='));
|
||||||
const match = argv[i].match(/--suite=(?<suitename>.+)/)
|
const match = argv[i].match(/--suite=(?<suitename>.+)/);
|
||||||
suiteFilter = match?.groups?.suitename ?? undefined;
|
suiteFilter = match?.groups?.suitename ?? undefined;
|
||||||
argv.splice(i, 1);
|
argv.splice(i, 1);
|
||||||
}
|
}
|
||||||
@@ -42,12 +42,18 @@ if (suiteFilter) {
|
|||||||
configs = configs.filter(e => e.name === suiteFilter);
|
configs = configs.filter(e => e.name === suiteFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (suiteFilter && configs.length === 0) {
|
||||||
|
console.error(`Unknown suite: ${suiteFilter}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
function npmBinScript(script) {
|
function npmBinScript(script) {
|
||||||
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
|
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
|
||||||
`${script}.cmd` : script));
|
`${script}.cmd` : script));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run() {
|
async function run() {
|
||||||
|
let exitCode = 0;
|
||||||
for (const config of configs) {
|
for (const config of configs) {
|
||||||
const command = npmBinScript('playwright');
|
const command = npmBinScript('playwright');
|
||||||
const args = ['test', '-c', config.path, ...argv];
|
const args = ['test', '-c', config.path, ...argv];
|
||||||
@@ -62,10 +68,13 @@ async function run() {
|
|||||||
|
|
||||||
if (run.error) {
|
if (run.error) {
|
||||||
console.error(run.error);
|
console.error(run.error);
|
||||||
process.exit(run.status ?? -1);
|
process.exit(run.status ?? 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
process.exit(run.status);
|
if (run.status) {
|
||||||
|
exitCode = run.status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
process.exit(exitCode);
|
||||||
}
|
}
|
||||||
run();
|
run();
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const ENC = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function printMenu() {
|
function printMenu() {
|
||||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
console.log('\x1b[2J\x1b[HTest mouse reports [Ctrl-C to exit]');
|
||||||
console.log();
|
console.log();
|
||||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||||
const protocols = Object.keys(PROTOCOLS);
|
const protocols = Object.keys(PROTOCOLS);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ if (process.argv.length > 2) {
|
|||||||
flagArgs = args.filter(e => e.startsWith('--'));
|
flagArgs = args.filter(e => e.startsWith('--'));
|
||||||
// ability to inject particular test files via
|
// ability to inject particular test files via
|
||||||
// npm run test [testFileA testFileB ...]
|
// npm run test [testFileA testFileB ...]
|
||||||
files = args.filter(e => !e.startsWith('--'));
|
const files = args.filter(e => !e.startsWith('--'));
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
testFiles = files;
|
testFiles = files;
|
||||||
}
|
}
|
||||||
@@ -39,8 +39,6 @@ if (checkCoverage) {
|
|||||||
...testFiles,
|
...testFiles,
|
||||||
...flagArgs
|
...flagArgs
|
||||||
];
|
];
|
||||||
console.info('executable', executable);
|
|
||||||
console.info('args', args);
|
|
||||||
const run = cp.spawnSync(
|
const run = cp.spawnSync(
|
||||||
executable,
|
executable,
|
||||||
args,
|
args,
|
||||||
@@ -51,7 +49,7 @@ if (checkCoverage) {
|
|||||||
stdio: 'inherit'
|
stdio: 'inherit'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
process.exit(run.status);
|
process.exit(run.status ?? -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = cp.spawnSync(
|
const run = cp.spawnSync(
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
|||||||
dlIip.appendChild(dtIip);
|
dlIip.appendChild(dtIip);
|
||||||
this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
|
this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
|
||||||
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
|
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
|
||||||
|
this._addDdWithButton(dlIip, 'image-demo5', 'rose (webp File)');
|
||||||
|
this._addDdWithButton(dlIip, 'image-demo6', 'kimono (avif MultipartFile)');
|
||||||
container.appendChild(dlIip);
|
container.appendChild(dlIip);
|
||||||
|
|
||||||
// Kitty demos
|
// Kitty demos
|
||||||
@@ -173,11 +175,15 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
|||||||
document.getElementById('image-demo2')!.addEventListener('click',
|
document.getElementById('image-demo2')!.addEventListener('click',
|
||||||
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
||||||
document.getElementById('image-demo3')!.addEventListener('click',
|
document.getElementById('image-demo3')!.addEventListener('click',
|
||||||
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/palette.png'));
|
||||||
document.getElementById('image-demo4')!.addEventListener('click',
|
document.getElementById('image-demo4')!.addEventListener('click',
|
||||||
iipDemoMulti('https://raw.githubusercontent.com/jerch/xterm-wasm-parts/master/fixtures/qoi/dice.qoi'));
|
iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/dice.qoi'));
|
||||||
|
document.getElementById('image-demo5')!.addEventListener('click',
|
||||||
|
iipDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/1_webp_a.webp'));
|
||||||
|
document.getElementById('image-demo6')!.addEventListener('click',
|
||||||
|
iipDemoMulti('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/testimages/kimono.crop.avif'));
|
||||||
document.getElementById('image-demo-kitty1')!.addEventListener('click',
|
document.getElementById('image-demo-kitty1')!.addEventListener('click',
|
||||||
kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
kittyDemo('https://raw.githubusercontent.com/xtermjs/xterm.js/master/addons/addon-image/fixture/palette.png'));
|
||||||
|
|
||||||
// demo for image retrieval API
|
// demo for image retrieval API
|
||||||
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
|
this._terminal.element!.addEventListener('click', (ev: MouseEvent) => {
|
||||||
|
|||||||
@@ -11,8 +11,35 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
|||||||
public readonly label = 'webgl';
|
public readonly label = 'webgl';
|
||||||
|
|
||||||
private _textureAtlasContainer!: HTMLElement;
|
private _textureAtlasContainer!: HTMLElement;
|
||||||
|
private _stressRunning = false;
|
||||||
|
private _scrollOutput: HTMLInputElement | undefined;
|
||||||
|
|
||||||
public build(container: HTMLElement): void {
|
public build(container: HTMLElement): void {
|
||||||
|
const stressButtons = document.createElement('div');
|
||||||
|
const stressStart = document.createElement('button');
|
||||||
|
stressStart.id = 'stress-start';
|
||||||
|
stressStart.textContent = 'atlas stress start';
|
||||||
|
stressStart.addEventListener('click', () => this._stress());
|
||||||
|
stressButtons.appendChild(stressStart);
|
||||||
|
|
||||||
|
const stressStop = document.createElement('button');
|
||||||
|
stressStop.id = 'stress-stop';
|
||||||
|
stressStop.textContent = 'atlas stress stop';
|
||||||
|
stressStop.addEventListener('click', () => this._stressRunning = false);
|
||||||
|
stressButtons.appendChild(stressStop);
|
||||||
|
container.appendChild(stressButtons);
|
||||||
|
|
||||||
|
const stressOptions = document.createElement('div');
|
||||||
|
this._scrollOutput = document.createElement('input');
|
||||||
|
this._scrollOutput.type = 'checkbox';
|
||||||
|
this._scrollOutput.id = 'scroll-output';
|
||||||
|
stressOptions.appendChild(this._scrollOutput);
|
||||||
|
const scrollOutputLabel = document.createElement('label');
|
||||||
|
scrollOutputLabel.htmlFor = 'scroll-output';
|
||||||
|
scrollOutputLabel.textContent = 'Scroll output';
|
||||||
|
stressOptions.appendChild(scrollOutputLabel);
|
||||||
|
container.appendChild(stressOptions);
|
||||||
|
|
||||||
const zoomCheckbox = document.createElement('input');
|
const zoomCheckbox = document.createElement('input');
|
||||||
zoomCheckbox.type = 'checkbox';
|
zoomCheckbox.type = 'checkbox';
|
||||||
zoomCheckbox.id = 'texture-atlas-zoom';
|
zoomCheckbox.id = 'texture-atlas-zoom';
|
||||||
@@ -48,4 +75,33 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
|||||||
canvas.style.width = `${canvas.width / dpr}px`;
|
canvas.style.width = `${canvas.width / dpr}px`;
|
||||||
canvas.style.height = `${canvas.height / dpr}px`;
|
canvas.style.height = `${canvas.height / dpr}px`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async _stress(): Promise<void> {
|
||||||
|
const TEXT = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
|
||||||
|
if (this._stressRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._stressRunning = true;
|
||||||
|
const scrollOutput = !!(this._scrollOutput?.checked);
|
||||||
|
for (let r = 0; r < 256; r += 2) {
|
||||||
|
for (let g = 0; g < 256; g += 2) {
|
||||||
|
let s: string[] = [];
|
||||||
|
for (let b = 0; b < 256; b += 2) {
|
||||||
|
if (!this._stressRunning) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rbg = `RGB: ${[r, g, b]}`;
|
||||||
|
s.push(`\r\x1b[38;2;${r};${g};${b}m${rbg.padEnd(18, ' ')}${TEXT}`);
|
||||||
|
if (s.length >= 16) {
|
||||||
|
if (scrollOutput) {
|
||||||
|
await new Promise<void>(r => this._terminal.write(s.join('\r\n') + '\r\n', r));
|
||||||
|
} else {
|
||||||
|
await new Promise<void>(r => this._terminal.write('\x1b[H' + s.join('\r\n'), r));
|
||||||
|
}
|
||||||
|
s = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -663,9 +663,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -680,9 +680,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm": {
|
"node_modules/@esbuild/android-arm": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -697,9 +697,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-arm64": {
|
"node_modules/@esbuild/android-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -714,9 +714,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/android-x64": {
|
"node_modules/@esbuild/android-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -731,9 +731,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-arm64": {
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -748,9 +748,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/darwin-x64": {
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -765,9 +765,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-arm64": {
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -782,9 +782,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/freebsd-x64": {
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -799,9 +799,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm": {
|
"node_modules/@esbuild/linux-arm": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
@@ -816,9 +816,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-arm64": {
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -833,9 +833,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ia32": {
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -850,9 +850,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-loong64": {
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
@@ -867,9 +867,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-mips64el": {
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"mips64el"
|
"mips64el"
|
||||||
],
|
],
|
||||||
@@ -884,9 +884,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-ppc64": {
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
@@ -901,9 +901,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-riscv64": {
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
@@ -918,9 +918,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-s390x": {
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
@@ -935,9 +935,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/linux-x64": {
|
"node_modules/@esbuild/linux-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -952,9 +952,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-arm64": {
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -969,9 +969,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/netbsd-x64": {
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -986,9 +986,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-arm64": {
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1003,9 +1003,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openbsd-x64": {
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1020,9 +1020,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/openharmony-arm64": {
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1037,9 +1037,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/sunos-x64": {
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -1054,9 +1054,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-arm64": {
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -1071,9 +1071,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-ia32": {
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"ia32"
|
"ia32"
|
||||||
],
|
],
|
||||||
@@ -1088,9 +1088,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/win32-x64": {
|
"node_modules/@esbuild/win32-x64": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -3061,16 +3061,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^4.0.2"
|
"balanced-match": "^4.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "18 || 20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/braces": {
|
"node_modules/braces": {
|
||||||
@@ -3519,15 +3519,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/concurrently": {
|
"node_modules/concurrently": {
|
||||||
"version": "9.2.1",
|
"version": "9.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz",
|
||||||
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
|
"integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "4.1.2",
|
"chalk": "4.1.2",
|
||||||
"rxjs": "7.8.2",
|
"rxjs": "7.8.2",
|
||||||
"shell-quote": "1.8.3",
|
"shell-quote": "1.9.0",
|
||||||
"supports-color": "8.1.1",
|
"supports-color": "8.1.1",
|
||||||
"tree-kill": "1.2.2",
|
"tree-kill": "1.2.2",
|
||||||
"yargs": "17.7.2"
|
"yargs": "17.7.2"
|
||||||
@@ -3869,9 +3869,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.28.0",
|
"version": "0.28.1",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -3882,32 +3882,32 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@esbuild/aix-ppc64": "0.28.0",
|
"@esbuild/aix-ppc64": "0.28.1",
|
||||||
"@esbuild/android-arm": "0.28.0",
|
"@esbuild/android-arm": "0.28.1",
|
||||||
"@esbuild/android-arm64": "0.28.0",
|
"@esbuild/android-arm64": "0.28.1",
|
||||||
"@esbuild/android-x64": "0.28.0",
|
"@esbuild/android-x64": "0.28.1",
|
||||||
"@esbuild/darwin-arm64": "0.28.0",
|
"@esbuild/darwin-arm64": "0.28.1",
|
||||||
"@esbuild/darwin-x64": "0.28.0",
|
"@esbuild/darwin-x64": "0.28.1",
|
||||||
"@esbuild/freebsd-arm64": "0.28.0",
|
"@esbuild/freebsd-arm64": "0.28.1",
|
||||||
"@esbuild/freebsd-x64": "0.28.0",
|
"@esbuild/freebsd-x64": "0.28.1",
|
||||||
"@esbuild/linux-arm": "0.28.0",
|
"@esbuild/linux-arm": "0.28.1",
|
||||||
"@esbuild/linux-arm64": "0.28.0",
|
"@esbuild/linux-arm64": "0.28.1",
|
||||||
"@esbuild/linux-ia32": "0.28.0",
|
"@esbuild/linux-ia32": "0.28.1",
|
||||||
"@esbuild/linux-loong64": "0.28.0",
|
"@esbuild/linux-loong64": "0.28.1",
|
||||||
"@esbuild/linux-mips64el": "0.28.0",
|
"@esbuild/linux-mips64el": "0.28.1",
|
||||||
"@esbuild/linux-ppc64": "0.28.0",
|
"@esbuild/linux-ppc64": "0.28.1",
|
||||||
"@esbuild/linux-riscv64": "0.28.0",
|
"@esbuild/linux-riscv64": "0.28.1",
|
||||||
"@esbuild/linux-s390x": "0.28.0",
|
"@esbuild/linux-s390x": "0.28.1",
|
||||||
"@esbuild/linux-x64": "0.28.0",
|
"@esbuild/linux-x64": "0.28.1",
|
||||||
"@esbuild/netbsd-arm64": "0.28.0",
|
"@esbuild/netbsd-arm64": "0.28.1",
|
||||||
"@esbuild/netbsd-x64": "0.28.0",
|
"@esbuild/netbsd-x64": "0.28.1",
|
||||||
"@esbuild/openbsd-arm64": "0.28.0",
|
"@esbuild/openbsd-arm64": "0.28.1",
|
||||||
"@esbuild/openbsd-x64": "0.28.0",
|
"@esbuild/openbsd-x64": "0.28.1",
|
||||||
"@esbuild/openharmony-arm64": "0.28.0",
|
"@esbuild/openharmony-arm64": "0.28.1",
|
||||||
"@esbuild/sunos-x64": "0.28.0",
|
"@esbuild/sunos-x64": "0.28.1",
|
||||||
"@esbuild/win32-arm64": "0.28.0",
|
"@esbuild/win32-arm64": "0.28.1",
|
||||||
"@esbuild/win32-ia32": "0.28.0",
|
"@esbuild/win32-ia32": "0.28.1",
|
||||||
"@esbuild/win32-x64": "0.28.0"
|
"@esbuild/win32-x64": "0.28.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/escalade": {
|
"node_modules/escalade": {
|
||||||
@@ -4308,9 +4308,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/express-ws/node_modules/ws": {
|
"node_modules/express-ws/node_modules/ws": {
|
||||||
"version": "7.5.10",
|
"version": "7.5.13",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
|
||||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
"integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -4351,9 +4351,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz",
|
||||||
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
"integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -4749,9 +4749,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5795,9 +5795,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mocha/node_modules/brace-expansion": {
|
"node_modules/mocha/node_modules/brace-expansion": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -6689,16 +6689,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/randombytes": {
|
|
||||||
"version": "2.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
|
||||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "^5.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/range-parser": {
|
"node_modules/range-parser": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
@@ -6932,27 +6922,6 @@
|
|||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/safer-buffer": {
|
"node_modules/safer-buffer": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
@@ -7078,13 +7047,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serialize-javascript": {
|
"node_modules/serialize-javascript": {
|
||||||
"version": "6.0.2",
|
"version": "7.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
|
||||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"engines": {
|
||||||
"randombytes": "^2.1.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/serve-static": {
|
"node_modules/serve-static": {
|
||||||
@@ -7158,9 +7127,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/shell-quote": {
|
"node_modules/shell-quote": {
|
||||||
"version": "1.8.3",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
|
||||||
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
|
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8006,9 +7975,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "7.25.0",
|
"version": "7.29.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz",
|
||||||
"integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==",
|
"integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -8483,9 +8452,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ws": {
|
"node_modules/ws": {
|
||||||
"version": "8.20.1",
|
"version": "8.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
"workspaces": [
|
"workspaces": [
|
||||||
"addons/*"
|
"addons/*"
|
||||||
],
|
],
|
||||||
|
"overrides": {
|
||||||
|
"serialize-javascript": "^7.0.3"
|
||||||
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"cli",
|
"cli",
|
||||||
"command-line",
|
"command-line",
|
||||||
@@ -34,7 +37,6 @@
|
|||||||
"start": "node demo/start",
|
"start": "node demo/start",
|
||||||
"dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo-client,esbuild-demo-server,server -c blue,yellow,cyan,green,magenta \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-client-watch\" \"npm:esbuild-demo-server-watch\" \"npm:start\"",
|
"dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo-client,esbuild-demo-server,server -c blue,yellow,cyan,green,magenta \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-client-watch\" \"npm:esbuild-demo-server-watch\" \"npm:start\"",
|
||||||
"build": "npm run tsc",
|
"build": "npm run tsc",
|
||||||
"build:parser-wasm": "bash bin/build_parser_wasm.sh",
|
|
||||||
"watch": "npm run tsc-watch",
|
"watch": "npm run tsc-watch",
|
||||||
"tsc": "tsgo -b ./tsconfig.all.json",
|
"tsc": "tsgo -b ./tsconfig.all.json",
|
||||||
"tsc-watch": "tsgo -b -w ./tsconfig.all.json --preserveWatchOutput",
|
"tsc-watch": "tsgo -b -w ./tsconfig.all.json --preserveWatchOutput",
|
||||||
@@ -62,9 +64,9 @@
|
|||||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||||
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
||||||
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
||||||
"benchmark": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
|
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
|
||||||
"benchmark-baseline": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
||||||
"benchmark-eval": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
||||||
"clean": "rm -rf lib out addons/*/lib addons/*/out",
|
"clean": "rm -rf lib out addons/*/lib addons/*/out",
|
||||||
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
|
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
private _keyDownHandled: boolean = false;
|
private _keyDownHandled: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Records whether a keydown event has occured since the last keyup event, i.e. whether a key
|
* Records whether a keydown event has occurred since the last keyup event, i.e. whether a key
|
||||||
* is currently "pressed".
|
* is currently "pressed".
|
||||||
*/
|
*/
|
||||||
private _keyDownSeen: boolean = false;
|
private _keyDownSeen: boolean = false;
|
||||||
@@ -492,6 +492,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
// https://issuetracker.google.com/issues/260170397
|
// https://issuetracker.google.com/issues/260170397
|
||||||
this.textarea.setAttribute('aria-multiline', 'false');
|
this.textarea.setAttribute('aria-multiline', 'false');
|
||||||
}
|
}
|
||||||
|
this.textarea.setAttribute('autocomplete', 'off');
|
||||||
this.textarea.setAttribute('autocorrect', 'off');
|
this.textarea.setAttribute('autocorrect', 'off');
|
||||||
this.textarea.setAttribute('autocapitalize', 'off');
|
this.textarea.setAttribute('autocapitalize', 'off');
|
||||||
this.textarea.setAttribute('spellcheck', 'false');
|
this.textarea.setAttribute('spellcheck', 'false');
|
||||||
@@ -505,7 +506,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
this.textarea,
|
this.textarea,
|
||||||
parent.ownerDocument.defaultView ?? window,
|
parent.ownerDocument.defaultView ?? window,
|
||||||
// Force unsafe null in node.js environment for tests
|
// Force unsafe null in node.js environment for tests
|
||||||
this._document ?? (typeof window !== 'undefined') ? window.document : null as any
|
this._document ?? ((typeof window !== 'undefined') ? window.document : null as any)
|
||||||
));
|
));
|
||||||
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
|
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
|
||||||
|
|
||||||
@@ -1072,10 +1073,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
|||||||
* Clear the entire buffer, making the prompt line the new first line.
|
* Clear the entire buffer, making the prompt line the new first line.
|
||||||
*/
|
*/
|
||||||
public clear(): void {
|
public clear(): void {
|
||||||
if (this.buffer.ybase === 0 && this.buffer.y === 0) {
|
|
||||||
// Don't clear if it's already clear
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.buffer.clearAllMarkers();
|
this.buffer.clearAllMarkers();
|
||||||
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
|
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
|
||||||
this.buffer.lines.length = 1;
|
this.buffer.lines.length = 1;
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import { OscLinkProvider } from './OscLinkProvider';
|
|||||||
import { ILink } from './Types';
|
import { ILink } from './Types';
|
||||||
import { createCellData, MockBufferService, MockOptionsService } from '../common/TestUtils.test';
|
import { createCellData, MockBufferService, MockOptionsService } from '../common/TestUtils.test';
|
||||||
import { IBufferService, IOscLinkService } from '../common/services/Services';
|
import { IBufferService, IOscLinkService } from '../common/services/Services';
|
||||||
import { IBufferLine, IOscLinkData } from '../common/Types';
|
import { IOscLinkData } from '../common/Types';
|
||||||
|
import { IBufferLine } from '../common/buffer/Types';
|
||||||
|
|
||||||
class TestOscLinkService implements IOscLinkService {
|
class TestOscLinkService implements IOscLinkService {
|
||||||
public serviceBrand: any;
|
public serviceBrand: any;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { IBufferRange, ILink } from './Types';
|
import { IBufferRange, ILink } from './Types';
|
||||||
import { ILinkProvider } from './services/Services';
|
import { ILinkProvider } from './services/Services';
|
||||||
import { CellData } from '../common/buffer/CellData';
|
import { CellData } from '../common/buffer/CellData';
|
||||||
import { IBufferLine } from '../common/Types';
|
import { IBufferLine } from '../common/buffer/Types';
|
||||||
import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services';
|
import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services';
|
||||||
|
|
||||||
export class OscLinkProvider implements ILinkProvider {
|
export class OscLinkProvider implements ILinkProvider {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (this._animationFrame) {
|
if (this._animationFrame !== undefined) {
|
||||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||||
this._animationFrame = undefined;
|
this._animationFrame = undefined;
|
||||||
}
|
}
|
||||||
@@ -31,9 +31,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
|||||||
|
|
||||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||||
this._refreshCallbacks.push(callback);
|
this._refreshCallbacks.push(callback);
|
||||||
if (!this._animationFrame) {
|
this._animationFrame ??= this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
||||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
|
||||||
}
|
|
||||||
return this._animationFrame;
|
return this._animationFrame;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
|||||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||||
|
|
||||||
if (this._animationFrame) {
|
if (this._animationFrame !== undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { assert } from 'chai';
|
|||||||
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';
|
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';
|
||||||
import { CellData } from '../common/buffer/CellData';
|
import { CellData } from '../common/buffer/CellData';
|
||||||
import { MockUnicodeService, createCellData } from '../common/TestUtils.test';
|
import { MockUnicodeService, createCellData } from '../common/TestUtils.test';
|
||||||
import { IMarker } from '../common/Types';
|
import { IMarker } from '../common/buffer/Types';
|
||||||
|
|
||||||
const INIT_COLS = 80;
|
const INIT_COLS = 80;
|
||||||
const INIT_ROWS = 24;
|
const INIT_ROWS = 24;
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration, I
|
|||||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
|
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
|
||||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from './renderer/shared/Types';
|
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from './renderer/shared/Types';
|
||||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from './Types';
|
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from './Types';
|
||||||
import { IBuffer, IBufferSet } from '../common/buffer/Types';
|
import { IAttributeData, IBuffer, IBufferLine, IBufferSet, ICellData } from '../common/buffer/Types';
|
||||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from '../common/Types';
|
import { ICircularList } from '../common/CircularList';
|
||||||
|
import { XtermListener, ICharset, ITerminalOptions, ColorIndex } from '../common/Types';
|
||||||
import { Buffer } from '../common/buffer/Buffer';
|
import { Buffer } from '../common/buffer/Buffer';
|
||||||
import * as Browser from '../common/Platform';
|
import * as Browser from '../common/Platform';
|
||||||
import { CoreBrowserTerminal } from './CoreBrowserTerminal';
|
import { CoreBrowserTerminal } from './CoreBrowserTerminal';
|
||||||
|
|||||||
@@ -3,10 +3,10 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
|
||||||
|
|
||||||
import { IRenderDebouncer } from './Types';
|
import { IRenderDebouncer } from './Types';
|
||||||
|
|
||||||
|
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounces calls to update screen readers to update at most once configurable interval of time.
|
* Debounces calls to update screen readers to update at most once configurable interval of time.
|
||||||
*/
|
*/
|
||||||
@@ -31,7 +31,9 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
|||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (this._refreshTimeoutID) {
|
if (this._refreshTimeoutID) {
|
||||||
clearTimeout(this._refreshTimeoutID);
|
clearTimeout(this._refreshTimeoutID);
|
||||||
|
this._refreshTimeoutID = undefined;
|
||||||
}
|
}
|
||||||
|
this._additionalRefreshRequested = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||||
@@ -47,7 +49,12 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
|||||||
// enough time to pass before refreshing again.
|
// enough time to pass before refreshing again.
|
||||||
const refreshRequestTime: number = performance.now();
|
const refreshRequestTime: number = performance.now();
|
||||||
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
|
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
|
||||||
// Enough time has lapsed since the last refresh; refresh immediately
|
// Enough time has elapsed since the last refresh; refresh immediately
|
||||||
|
if (this._refreshTimeoutID !== undefined) {
|
||||||
|
clearTimeout(this._refreshTimeoutID);
|
||||||
|
this._refreshTimeoutID = undefined;
|
||||||
|
this._additionalRefreshRequested = false;
|
||||||
|
}
|
||||||
this._lastRefreshMs = refreshRequestTime;
|
this._lastRefreshMs = refreshRequestTime;
|
||||||
this._innerRefresh();
|
this._innerRefresh();
|
||||||
} else if (!this._additionalRefreshRequested) {
|
} else if (!this._additionalRefreshRequested) {
|
||||||
|
|||||||
@@ -3,8 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from '../common/Types';
|
import { IColor, ITerminalOptions } from '../common/Types';
|
||||||
import { IBuffer } from '../common/buffer/Types';
|
import { CharData, IBuffer } from '../common/buffer/Types';
|
||||||
|
import { ICoreTerminal } from '../common/CoreTerminal';
|
||||||
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
|
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
|
||||||
import { channels, css } from '../common/Color';
|
import { channels, css } from '../common/Color';
|
||||||
import type { IEvent } from '../common/Event';
|
import type { IEvent } from '../common/Event';
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||||
const rect = element.getBoundingClientRect();
|
const rect = element.getBoundingClientRect();
|
||||||
const elementStyle = window.getComputedStyle(element);
|
const elementStyle = window.getComputedStyle(element);
|
||||||
const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left'));
|
const leftPadding = parseInt(elementStyle.getPropertyValue('padding-left'), 10);
|
||||||
const topPadding = parseInt(elementStyle.getPropertyValue('padding-top'));
|
const topPadding = parseInt(elementStyle.getPropertyValue('padding-top'), 10);
|
||||||
return [
|
return [
|
||||||
event.clientX - rect.left - leftPadding,
|
event.clientX - rect.left - leftPadding,
|
||||||
event.clientY - rect.top - topPadding
|
event.clientY - rect.top - topPadding
|
||||||
@@ -22,7 +22,7 @@ export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyl
|
|||||||
* @param event The mouse event.
|
* @param event The mouse event.
|
||||||
* @param element The terminal's container element.
|
* @param element The terminal's container element.
|
||||||
* @param colCount The number of columns in the terminal.
|
* @param colCount The number of columns in the terminal.
|
||||||
* @param rowCount The number of rows n the terminal.
|
* @param rowCount The number of rows in the terminal.
|
||||||
* @param hasValidCharSize Whether there is a valid character size available.
|
* @param hasValidCharSize Whether there is a valid character size available.
|
||||||
* @param cssCellWidth The cell width device pixel render dimensions.
|
* @param cssCellWidth The cell width device pixel render dimensions.
|
||||||
* @param cssCellHeight The cell height device pixel render dimensions.
|
* @param cssCellHeight The cell height device pixel render dimensions.
|
||||||
@@ -31,21 +31,17 @@ export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyl
|
|||||||
* select that cell and the right half will select the next cell.
|
* select that cell and the right half will select the next cell.
|
||||||
*/
|
*/
|
||||||
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: Pick<MouseEvent, 'clientX' | 'clientY'>, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, cssCellWidth: number, cssCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: Pick<MouseEvent, 'clientX' | 'clientY'>, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, cssCellWidth: number, cssCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||||
// Coordinates cannot be measured if there are no valid
|
// Coordinates cannot be measured if there is no valid character size.
|
||||||
if (!hasValidCharSize) {
|
if (!hasValidCharSize) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coords = getCoordsRelativeToElement(window, event, element);
|
const coords = getCoordsRelativeToElement(window, event, element);
|
||||||
if (!coords) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth);
|
coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth);
|
||||||
coords[1] = Math.ceil(coords[1] / cssCellHeight);
|
coords[1] = Math.ceil(coords[1] / cssCellHeight);
|
||||||
|
|
||||||
// Ensure coordinates are within the terminal viewport. Note that selections
|
// Ensure coordinates are within the terminal viewport. Note that selections
|
||||||
// need an addition point of precision to cover the end point (as characters
|
// need an additional point of precision to cover the end point (as characters
|
||||||
// cover half of one char and half of the next).
|
// cover half of one char and half of the next).
|
||||||
coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0));
|
coords[0] = Math.min(Math.max(coords[0], 1), colCount + (isSelection ? 1 : 0));
|
||||||
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
|
coords[1] = Math.min(Math.max(coords[1], 1), rowCount);
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ function wrappedRowsForRow(currentRow: number, bufferService: IBufferService): n
|
|||||||
*/
|
*/
|
||||||
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
|
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
|
||||||
let startRow;
|
let startRow;
|
||||||
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
|
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
|
||||||
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||||
} else {
|
} else {
|
||||||
startRow = startY;
|
startRow = startY;
|
||||||
|
|||||||
@@ -536,9 +536,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
|||||||
for (let y = start; y <= end; y++) {
|
for (let y = start; y <= end; y++) {
|
||||||
const row = y + buffer.ydisp;
|
const row = y + buffer.ydisp;
|
||||||
const rowElement = this._rowElements[y];
|
const rowElement = this._rowElements[y];
|
||||||
|
if (!rowElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const lineData = buffer.lines.get(row);
|
const lineData = buffer.lines.get(row);
|
||||||
if (!rowElement || !lineData) {
|
if (!lineData) {
|
||||||
break;
|
rowElement.replaceChildren();
|
||||||
|
this._setRowBlinkState(y, false);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
rowElement.replaceChildren(
|
rowElement.replaceChildren(
|
||||||
...this._rowFactory.createRow(
|
...this._rowFactory.createRow(
|
||||||
@@ -610,9 +615,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
|||||||
for (let i = y; i <= y2; ++i) {
|
for (let i = y; i <= y2; ++i) {
|
||||||
const row = i + buffer.ydisp;
|
const row = i + buffer.ydisp;
|
||||||
const rowElement = this._rowElements[i];
|
const rowElement = this._rowElements[i];
|
||||||
|
if (!rowElement) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const bufferline = buffer.lines.get(row);
|
const bufferline = buffer.lines.get(row);
|
||||||
if (!rowElement || !bufferline) {
|
if (!bufferline) {
|
||||||
break;
|
rowElement.replaceChildren();
|
||||||
|
this._setRowBlinkState(i, false);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
rowElement.replaceChildren(
|
rowElement.replaceChildren(
|
||||||
...this._rowFactory.createRow(
|
...this._rowFactory.createRow(
|
||||||
@@ -694,7 +704,7 @@ class CursorBlinkStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _clearIdleTimer(): void {
|
private _clearIdleTimer(): void {
|
||||||
if (this._idleTimeout) {
|
if (this._idleTimeout !== undefined) {
|
||||||
this._coreBrowserService.window.clearTimeout(this._idleTimeout);
|
this._coreBrowserService.window.clearTimeout(this._idleTimeout);
|
||||||
this._idleTimeout = undefined;
|
this._idleTimeout = undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,14 @@
|
|||||||
import jsdom = require('jsdom');
|
import jsdom = require('jsdom');
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
import { DomRendererRowFactory } from './DomRendererRowFactory';
|
||||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/buffer/Constants';
|
import { DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/buffer/Constants';
|
||||||
import { BufferLine, DEFAULT_ATTR_DATA } from '../../../common/buffer/BufferLine';
|
import { BufferLine, DEFAULT_ATTR_DATA } from '../../../common/buffer/BufferLine';
|
||||||
import { BufferLineStringCache } from '../../../common/buffer/BufferLineStringCache';
|
import { IBufferLine } from '../../../common/buffer/Types';
|
||||||
import { IBufferLine } from '../../../common/Types';
|
|
||||||
import { CellData } from '../../../common/buffer/CellData';
|
import { CellData } from '../../../common/buffer/CellData';
|
||||||
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test';
|
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test';
|
||||||
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test';
|
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test';
|
||||||
import { TestWidthCache } from './WidthCache.test';
|
import { TestWidthCache } from './WidthCache.test';
|
||||||
|
|
||||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
|
||||||
|
|
||||||
describe('DomRendererRowFactory', () => {
|
describe('DomRendererRowFactory', () => {
|
||||||
let dom: jsdom.JSDOM;
|
let dom: jsdom.JSDOM;
|
||||||
@@ -517,7 +515,7 @@ describe('DomRendererRowFactory', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyLineData(cols: number): IBufferLine {
|
function createEmptyLineData(cols: number): IBufferLine {
|
||||||
const lineData = new BufferLine(TEST_STRING_CACHE, cols);
|
const lineData = new BufferLine(cols);
|
||||||
for (let i = 0; i < cols; i++) {
|
for (let i = 0; i < cols; i++) {
|
||||||
lineData.setCell(i, NULL_CELL_DATA);
|
lineData.setCell(i, NULL_CELL_DATA);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IBufferLine, ICellData, IColor } from '../../../common/Types';
|
import { IColor } from '../../../common/Types';
|
||||||
|
import { IBufferLine, ICellData } from '../../../common/buffer/Types';
|
||||||
import { INVERTED_DEFAULT_COLOR } from '../shared/Constants';
|
import { INVERTED_DEFAULT_COLOR } from '../shared/Constants';
|
||||||
import { WHITESPACE_CELL_CHAR, Attributes } from '../../../common/buffer/Constants';
|
import { WHITESPACE_CELL_CHAR, Attributes } from '../../../common/buffer/Constants';
|
||||||
import { CellData } from '../../../common/buffer/CellData';
|
import { CellData } from '../../../common/buffer/CellData';
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ export class StandardWheelEvent {
|
|||||||
let shouldFactorDPR: boolean = false;
|
let shouldFactorDPR: boolean = false;
|
||||||
if (platform.isChrome) {
|
if (platform.isChrome) {
|
||||||
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
|
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
|
||||||
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
|
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1], 10) : 123;
|
||||||
shouldFactorDPR = chromeMajorVersion <= 122;
|
shouldFactorDPR = chromeMajorVersion <= 122;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,13 @@ describe('SelectionModel', () => {
|
|||||||
assert.deepEqual(model.finalSelectionStart, undefined);
|
assert.deepEqual(model.finalSelectionStart, undefined);
|
||||||
assert.deepEqual(model.finalSelectionEnd, undefined);
|
assert.deepEqual(model.finalSelectionEnd, undefined);
|
||||||
});
|
});
|
||||||
|
it('should reset selection start to origin when start row is trimmed', () => {
|
||||||
|
model.selectionStart = [50, 0];
|
||||||
|
model.selectionEnd = [10, 2];
|
||||||
|
assert.equal(model.handleTrim(1), true);
|
||||||
|
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('finalSelectionStart', () => {
|
describe('finalSelectionStart', () => {
|
||||||
|
|||||||
@@ -135,9 +135,10 @@ export class SelectionModel {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the selection start is trimmed, ensure the start column is 0.
|
// If the selection start row is trimmed away, reset to the buffer origin.
|
||||||
if (this.selectionStart && this.selectionStart[1] < 0) {
|
if (this.selectionStart && this.selectionStart[1] < 0) {
|
||||||
this.selectionStart[1] = 0;
|
this.selectionStart = [0, 0];
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,10 @@ import { assert } from 'chai';
|
|||||||
import { ICharacterJoinerService } from './Services';
|
import { ICharacterJoinerService } from './Services';
|
||||||
import { CharacterJoinerService } from './CharacterJoinerService';
|
import { CharacterJoinerService } from './CharacterJoinerService';
|
||||||
import { BufferLine } from '../../common/buffer/BufferLine';
|
import { BufferLine } from '../../common/buffer/BufferLine';
|
||||||
import { BufferLineStringCache } from '../../common/buffer/BufferLineStringCache';
|
import { IBufferLine } from '../../common/buffer/Types';
|
||||||
import { IBufferLine } from '../../common/Types';
|
|
||||||
import { CellData } from '../../common/buffer/CellData';
|
import { CellData } from '../../common/buffer/CellData';
|
||||||
import { MockBufferService, createCellData } from '../../common/TestUtils.test';
|
import { MockBufferService, createCellData } from '../../common/TestUtils.test';
|
||||||
|
|
||||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
|
||||||
|
|
||||||
describe('CharacterJoinerService', () => {
|
describe('CharacterJoinerService', () => {
|
||||||
let service: ICharacterJoinerService;
|
let service: ICharacterJoinerService;
|
||||||
@@ -25,7 +23,7 @@ describe('CharacterJoinerService', () => {
|
|||||||
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
||||||
|
|
||||||
lines.set(3, lineData([['no joined ranges']]));
|
lines.set(3, lineData([['no joined ranges']]));
|
||||||
lines.set(4, new BufferLine(TEST_STRING_CACHE, 0));
|
lines.set(4, new BufferLine(0));
|
||||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||||
const line6 = lineData([['wi']]);
|
const line6 = lineData([['wi']]);
|
||||||
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
line6.resize(line6.length + 1, createCellData(0, '¥', 2));
|
||||||
@@ -270,7 +268,7 @@ describe('CharacterJoinerService', () => {
|
|||||||
type IPartialLineData = ([string] | [string, number]);
|
type IPartialLineData = ([string] | [string, number]);
|
||||||
|
|
||||||
function lineData(data: IPartialLineData[]): IBufferLine {
|
function lineData(data: IPartialLineData[]): IBufferLine {
|
||||||
const tline = new BufferLine(TEST_STRING_CACHE, 0);
|
const tline = new BufferLine(0);
|
||||||
for (let i = 0; i < data.length; ++i) {
|
for (let i = 0; i < data.length; ++i) {
|
||||||
const line = data[i][0];
|
const line = data[i][0];
|
||||||
const attr = (data[i][1] || 0) as number;
|
const attr = (data[i][1] || 0) as number;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IBufferLine, ICellData, CharData } from '../../common/Types';
|
import { CharData, IBufferLine, ICellData } from '../../common/buffer/Types';
|
||||||
import { ICharacterJoiner } from '../Types';
|
import { ICharacterJoiner } from '../Types';
|
||||||
import { AttributeData } from '../../common/buffer/AttributeData';
|
import { AttributeData } from '../../common/buffer/AttributeData';
|
||||||
import { WHITESPACE_CELL_CHAR, Content } from '../../common/buffer/Constants';
|
import { WHITESPACE_CELL_CHAR, Content } from '../../common/buffer/Constants';
|
||||||
@@ -100,6 +100,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
|
|||||||
|
|
||||||
const ranges: [number, number][] = [];
|
const ranges: [number, number][] = [];
|
||||||
const lineStr = line.translateToString(true);
|
const lineStr = line.translateToString(true);
|
||||||
|
const trimmedLength = line.getTrimmedLength();
|
||||||
|
|
||||||
// Because some cells can be represented by multiple javascript characters,
|
// Because some cells can be represented by multiple javascript characters,
|
||||||
// we track the cell and the string indexes separately. This allows us to
|
// we track the cell and the string indexes separately. This allows us to
|
||||||
@@ -111,7 +112,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
|
|||||||
let rangeAttrFG = line.getFg(0);
|
let rangeAttrFG = line.getFg(0);
|
||||||
let rangeAttrBG = line.getBg(0);
|
let rangeAttrBG = line.getBg(0);
|
||||||
|
|
||||||
for (let x = 0; x < line.getTrimmedLength(); x++) {
|
for (let x = 0; x < trimmedLength; x++) {
|
||||||
line.loadCell(x, this._workCell);
|
line.loadCell(x, this._workCell);
|
||||||
|
|
||||||
if (this._workCell.getWidth() === 0) {
|
if (this._workCell.getWidth() === 0) {
|
||||||
@@ -147,7 +148,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process any trailing ranges.
|
// Process any trailing ranges.
|
||||||
if (this._bufferService.cols - rangeStartColumn > 1) {
|
if (trimmedLength - rangeStartColumn > 1) {
|
||||||
const joinedRanges = this._getJoinedRanges(
|
const joinedRanges = this._getJoinedRanges(
|
||||||
lineStr,
|
lineStr,
|
||||||
rangeStartStringIndex,
|
rangeStartStringIndex,
|
||||||
@@ -216,7 +217,8 @@ export class CharacterJoinerService implements ICharacterJoinerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let x = startCol; x < this._bufferService.cols; x++) {
|
const trimmedLength = line.getTrimmedLength();
|
||||||
|
for (let x = startCol; x < trimmedLength; x++) {
|
||||||
const width = line.getWidth(x);
|
const width = line.getWidth(x);
|
||||||
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
|
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
|
||||||
|
|
||||||
@@ -264,7 +266,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
|
|||||||
// If there is still a range left at the end, it must extend all the way to
|
// If there is still a range left at the end, it must extend all the way to
|
||||||
// the end of the line.
|
// the end of the line.
|
||||||
if (currentRange) {
|
if (currentRange) {
|
||||||
currentRange[1] = this._bufferService.cols;
|
currentRange[1] = trimmedLength;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ describe('MouseService _triggerMouseEvent', () => {
|
|||||||
for (let i = 0; i < bufferService.cols; ++i) {
|
for (let i = 0; i < bufferService.cols; ++i) {
|
||||||
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||||
if (i > 222) {
|
if (i > 222) {
|
||||||
// supress mouse reports if we are out of addressible range (max. 222)
|
// suppress mouse reports if we are out of addressable range (max. 222)
|
||||||
assert.deepEqual(toBytes(reports.pop()), []);
|
assert.deepEqual(toBytes(reports.pop()), []);
|
||||||
} else {
|
} else {
|
||||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||||
@@ -351,3 +351,96 @@ describe('MouseService mouseEventsRequireAlt', () => {
|
|||||||
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
|
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('MouseService multi-window', () => {
|
||||||
|
interface IRecordingDocument {
|
||||||
|
doc: Document;
|
||||||
|
added: string[];
|
||||||
|
removed: string[];
|
||||||
|
listeners: { [type: string]: EventListener[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRecordingDocument(): IRecordingDocument {
|
||||||
|
const added: string[] = [];
|
||||||
|
const removed: string[] = [];
|
||||||
|
const listeners: { [type: string]: EventListener[] } = {};
|
||||||
|
const doc = {
|
||||||
|
addEventListener: (type: string, listener: EventListener) => {
|
||||||
|
added.push(type);
|
||||||
|
(listeners[type] ??= []).push(listener);
|
||||||
|
},
|
||||||
|
removeEventListener: (type: string) => removed.push(type)
|
||||||
|
} as any;
|
||||||
|
return { doc, added, removed, listeners };
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should manage protocol mouseup/mousemove listeners on the element\'s current document, not the document the terminal was opened with', () => {
|
||||||
|
const mouseStateService = new MouseStateService();
|
||||||
|
const optionsService = new OptionsService({});
|
||||||
|
const mouseService = new MouseService(
|
||||||
|
new MockRenderService(),
|
||||||
|
{
|
||||||
|
getMouseReportCoords: () => ({ col: 0, row: 0, x: 0, y: 0 })
|
||||||
|
} as any,
|
||||||
|
mouseStateService,
|
||||||
|
{
|
||||||
|
triggerDataEvent: () => {},
|
||||||
|
triggerBinaryEvent: () => {},
|
||||||
|
decPrivateModes: { applicationCursorKeys: false }
|
||||||
|
} as any,
|
||||||
|
bufferService,
|
||||||
|
optionsService,
|
||||||
|
new TestSelectionService(),
|
||||||
|
logService,
|
||||||
|
new MockCoreBrowserService()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Simulate a terminal that was opened with one document but whose element now lives in
|
||||||
|
// another document (eg. it was moved to a child window)
|
||||||
|
const openDocument = createRecordingDocument();
|
||||||
|
const childDocument = createRecordingDocument();
|
||||||
|
const elementListeners: { [type: string]: EventListener[] } = {};
|
||||||
|
const element = {
|
||||||
|
classList: {
|
||||||
|
add: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
contains: () => false
|
||||||
|
},
|
||||||
|
addEventListener: (type: string, listener: EventListener) => {
|
||||||
|
(elementListeners[type] ??= []).push(listener);
|
||||||
|
},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
ownerDocument: childDocument.doc
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
mouseService.bindMouse({
|
||||||
|
element,
|
||||||
|
screenElement: createTestMouseTargetElement(),
|
||||||
|
document: openDocument.doc
|
||||||
|
}, disposable => disposable, () => {});
|
||||||
|
mouseStateService.activeProtocol = 'ANY';
|
||||||
|
|
||||||
|
elementListeners['mousedown'][0]({
|
||||||
|
type: 'mousedown',
|
||||||
|
button: 0,
|
||||||
|
buttons: 1,
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
preventDefault: () => {}
|
||||||
|
} as any);
|
||||||
|
assert.deepEqual(childDocument.added, ['mouseup', 'mousemove']);
|
||||||
|
assert.deepEqual(openDocument.added, []);
|
||||||
|
|
||||||
|
childDocument.listeners['mouseup'][0]({
|
||||||
|
type: 'mouseup',
|
||||||
|
button: 0,
|
||||||
|
buttons: 0,
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
shiftKey: false
|
||||||
|
} as any);
|
||||||
|
assert.deepEqual(childDocument.removed, ['mouseup', 'mousemove']);
|
||||||
|
assert.deepEqual(openDocument.removed, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { addDisposableListener } from '../Dom';
|
|||||||
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from '../../common/services/Services';
|
import { IBufferService, IMouseStateService, ICoreService, ILogService, IOptionsService } from '../../common/services/Services';
|
||||||
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from '../../common/Types';
|
import { CoreMouseAction, CoreMouseButton, CoreMouseEventType, ICoreMouseEvent, IDisposable } from '../../common/Types';
|
||||||
import { C0 } from '../../common/data/EscapeSequences';
|
import { C0 } from '../../common/data/EscapeSequences';
|
||||||
import { DisposableStore, MutableDisposable, toDisposable } from '../../common/Lifecycle';
|
import { DisposableStore, MutableDisposable } from '../../common/Lifecycle';
|
||||||
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
import { ICoreBrowserService, IMouseCoordsService, IMouseService, IMouseServiceTarget, IRenderService, ISelectionService } from './Services';
|
||||||
import { Gesture, EventType as GestureEventType, IGestureEvent } from '../scrollable/touch';
|
import { Gesture, EventType as GestureEventType, IGestureEvent } from '../scrollable/touch';
|
||||||
|
|
||||||
@@ -21,6 +21,8 @@ interface IMouseBindContext {
|
|||||||
readonly target: IMouseServiceTarget;
|
readonly target: IMouseServiceTarget;
|
||||||
readonly focus: () => void;
|
readonly focus: () => void;
|
||||||
readonly requestedEvents: RequestedMouseEvents;
|
readonly requestedEvents: RequestedMouseEvents;
|
||||||
|
readonly mouseupListener: MutableDisposable<IDisposable>;
|
||||||
|
readonly mousedragListener: MutableDisposable<IDisposable>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MouseService implements IMouseService {
|
export class MouseService implements IMouseService {
|
||||||
@@ -61,7 +63,11 @@ export class MouseService implements IMouseService {
|
|||||||
mousedrag: null,
|
mousedrag: null,
|
||||||
mousemove: null
|
mousemove: null
|
||||||
};
|
};
|
||||||
const ctx: IMouseBindContext = { target, focus, requestedEvents };
|
const mouseupListener = new MutableDisposable<IDisposable>();
|
||||||
|
const mousedragListener = new MutableDisposable<IDisposable>();
|
||||||
|
register(mouseupListener);
|
||||||
|
register(mousedragListener);
|
||||||
|
const ctx: IMouseBindContext = { target, focus, requestedEvents, mouseupListener, mousedragListener };
|
||||||
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
||||||
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
||||||
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
||||||
@@ -85,16 +91,6 @@ export class MouseService implements IMouseService {
|
|||||||
// force initial onProtocolChange so we dont miss early mouse requests
|
// force initial onProtocolChange so we dont miss early mouse requests
|
||||||
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
|
this._mouseStateService.activeProtocol = this._mouseStateService.activeProtocol;
|
||||||
|
|
||||||
// Ensure document-level listeners are removed on dispose
|
|
||||||
register(toDisposable(() => {
|
|
||||||
if (requestedEvents.mouseup) {
|
|
||||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
|
||||||
}
|
|
||||||
if (requestedEvents.mousedrag) {
|
|
||||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Always on" event listeners.
|
* "Always on" event listeners.
|
||||||
*/
|
*/
|
||||||
@@ -199,12 +195,8 @@ export class MouseService implements IMouseService {
|
|||||||
this._sendEvent(ctx, ev);
|
this._sendEvent(ctx, ev);
|
||||||
if (!ev.buttons) {
|
if (!ev.buttons) {
|
||||||
// if no other button is held remove global handlers
|
// if no other button is held remove global handlers
|
||||||
if (ctx.requestedEvents.mouseup) {
|
ctx.mouseupListener.clear();
|
||||||
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
ctx.mousedragListener.clear();
|
||||||
}
|
|
||||||
if (ctx.requestedEvents.mousedrag) {
|
|
||||||
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,11 +238,14 @@ export class MouseService implements IMouseService {
|
|||||||
// of the terminal element.
|
// of the terminal element.
|
||||||
// Note: Other emulators also do this for 'mousedown' while a button
|
// Note: Other emulators also do this for 'mousedown' while a button
|
||||||
// is held, we currently limit 'mousedown' to the terminal only.
|
// is held, we currently limit 'mousedown' to the terminal only.
|
||||||
|
// Use the element's current document in case it moved to another window after open.
|
||||||
|
const { element, document: targetDocument } = ctx.target;
|
||||||
|
const listenerDocument = element.ownerDocument ?? targetDocument;
|
||||||
if (ctx.requestedEvents.mouseup) {
|
if (ctx.requestedEvents.mouseup) {
|
||||||
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
ctx.mouseupListener.value = addDisposableListener(listenerDocument, 'mouseup', ctx.requestedEvents.mouseup);
|
||||||
}
|
}
|
||||||
if (ctx.requestedEvents.mousedrag) {
|
if (ctx.requestedEvents.mousedrag) {
|
||||||
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
ctx.mousedragListener.value = addDisposableListener(listenerDocument, 'mousemove', ctx.requestedEvents.mousedrag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,7 +393,7 @@ export class MouseService implements IMouseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
||||||
const { element, document } = ctx.target;
|
const { element } = ctx.target;
|
||||||
const { requestedEvents } = ctx;
|
const { requestedEvents } = ctx;
|
||||||
// apply global changes on events
|
// apply global changes on events
|
||||||
if (events) {
|
if (events) {
|
||||||
@@ -433,18 +428,14 @@ export class MouseService implements IMouseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!(events & CoreMouseEventType.UP)) {
|
if (!(events & CoreMouseEventType.UP)) {
|
||||||
if (requestedEvents.mouseup) {
|
ctx.mouseupListener.clear();
|
||||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
|
||||||
}
|
|
||||||
requestedEvents.mouseup = null;
|
requestedEvents.mouseup = null;
|
||||||
} else {
|
} else {
|
||||||
requestedEvents.mouseup ??= eventListeners.mouseup;
|
requestedEvents.mouseup ??= eventListeners.mouseup;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(events & CoreMouseEventType.DRAG)) {
|
if (!(events & CoreMouseEventType.DRAG)) {
|
||||||
if (requestedEvents.mousedrag) {
|
ctx.mousedragListener.clear();
|
||||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
|
||||||
}
|
|
||||||
requestedEvents.mousedrag = null;
|
requestedEvents.mousedrag = null;
|
||||||
} else {
|
} else {
|
||||||
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
requestedEvents.mousedrag ??= eventListeners.mousedrag;
|
||||||
|
|||||||
@@ -6,17 +6,14 @@
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SelectionService, SelectionMode } from './SelectionService';
|
import { SelectionService, SelectionMode } from './SelectionService';
|
||||||
import { SelectionModel } from '../selection/SelectionModel';
|
import { SelectionModel } from '../selection/SelectionModel';
|
||||||
import { IBufferLine } from '../../common/Types';
|
import { IBuffer, IBufferLine } from '../../common/buffer/Types';
|
||||||
import { MockBufferService, MockOptionsService, MockCoreService, MockMouseStateService, createCellData } from '../../common/TestUtils.test';
|
import { MockBufferService, MockOptionsService, MockCoreService, MockMouseStateService, createCellData } from '../../common/TestUtils.test';
|
||||||
import { BufferLine } from '../../common/buffer/BufferLine';
|
import { BufferLine } from '../../common/buffer/BufferLine';
|
||||||
import { BufferLineStringCache } from '../../common/buffer/BufferLineStringCache';
|
|
||||||
import { IBufferService, IOptionsService } from '../../common/services/Services';
|
import { IBufferService, IOptionsService } from '../../common/services/Services';
|
||||||
import { MockCoreBrowserService, MockMouseService, MockRenderService } from '../TestUtils.test';
|
import { MockCoreBrowserService, MockMouseService, MockRenderService } from '../TestUtils.test';
|
||||||
import { CellData } from '../../common/buffer/CellData';
|
import { CellData } from '../../common/buffer/CellData';
|
||||||
import { IBuffer } from '../../common/buffer/Types';
|
|
||||||
import { IRenderService } from './Services';
|
import { IRenderService } from './Services';
|
||||||
|
|
||||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
|
||||||
|
|
||||||
class TestSelectionService extends SelectionService {
|
class TestSelectionService extends SelectionService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -61,7 +58,7 @@ describe('SelectionService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function stringToRow(text: string): IBufferLine {
|
function stringToRow(text: string): IBufferLine {
|
||||||
const result = new BufferLine(TEST_STRING_CACHE, text.length);
|
const result = new BufferLine(text.length);
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
result.setCell(i, createCellData(0, text.charAt(i), 1));
|
||||||
}
|
}
|
||||||
@@ -69,7 +66,7 @@ describe('SelectionService', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||||
const line = new BufferLine(TEST_STRING_CACHE, chars.length);
|
const line = new BufferLine(chars.length);
|
||||||
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
chars.map((c, idx) => line.setCell(idx, createCellData(0, c, 1)));
|
||||||
return line;
|
return line;
|
||||||
}
|
}
|
||||||
@@ -124,7 +121,7 @@ describe('SelectionService', () => {
|
|||||||
[0, 'o', 1, 'o'.charCodeAt(0)],
|
[0, 'o', 1, 'o'.charCodeAt(0)],
|
||||||
[0, 'o', 1, 'o'.charCodeAt(0)]
|
[0, 'o', 1, 'o'.charCodeAt(0)]
|
||||||
];
|
];
|
||||||
const line = new BufferLine(TEST_STRING_CACHE, data.length);
|
const line = new BufferLine(data.length);
|
||||||
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
|
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
|
||||||
buffer.lines.set(0, line);
|
buffer.lines.set(0, line);
|
||||||
// Ensure wide characters take up 2 columns
|
// Ensure wide characters take up 2 columns
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from
|
|||||||
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from './Services';
|
import { ICoreBrowserService, IMouseCoordsService, IRenderService, ISelectionService } from './Services';
|
||||||
import { Disposable, MutableDisposable, toDisposable } from '../../common/Lifecycle';
|
import { Disposable, MutableDisposable, toDisposable } from '../../common/Lifecycle';
|
||||||
import * as Browser from '../../common/Platform';
|
import * as Browser from '../../common/Platform';
|
||||||
import { IBufferLine, ICellData, IDisposable } from '../../common/Types';
|
import { IDisposable } from '../../common/Types';
|
||||||
|
import { IBuffer, IBufferLine, ICellData } from '../../common/buffer/Types';
|
||||||
import { getRangeLength } from '../../common/buffer/BufferRange';
|
import { getRangeLength } from '../../common/buffer/BufferRange';
|
||||||
import { CellData } from '../../common/buffer/CellData';
|
import { CellData } from '../../common/buffer/CellData';
|
||||||
import { IBuffer } from '../../common/buffer/Types';
|
|
||||||
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from '../../common/services/Services';
|
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from '../../common/services/Services';
|
||||||
import { Emitter } from '../../common/Event';
|
import { Emitter } from '../../common/Event';
|
||||||
|
|
||||||
@@ -704,7 +704,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
|||||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
this._model.selectionEnd[0] = this._bufferService.cols;
|
this._model.selectionEnd[0] = this._bufferService.cols;
|
||||||
}
|
}
|
||||||
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1);
|
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows - 1, buffer.lines.length - 1);
|
||||||
} else {
|
} else {
|
||||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
this._model.selectionEnd[0] = 0;
|
this._model.selectionEnd[0] = 0;
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export class ThemeService extends Disposable implements IThemeService {
|
|||||||
colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
|
colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Clear our the cache
|
// Clear the cache
|
||||||
this._contrastCache.clear();
|
this._contrastCache.clear();
|
||||||
this._halfContrastCache.clear();
|
this._halfContrastCache.clear();
|
||||||
this._updateRestoreColors();
|
this._updateRestoreColors();
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICircularList } from './Types';
|
|
||||||
import { Disposable } from './Lifecycle';
|
import { Disposable } from './Lifecycle';
|
||||||
import { Emitter } from './Event';
|
import { Emitter, type IEvent } from './Event';
|
||||||
|
|
||||||
export interface IInsertEvent {
|
export interface IInsertEvent {
|
||||||
index: number;
|
index: number;
|
||||||
@@ -17,6 +16,28 @@ export interface IDeleteEvent {
|
|||||||
amount: number;
|
amount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ICircularList<T> {
|
||||||
|
length: number;
|
||||||
|
maxLength: number;
|
||||||
|
isFull: boolean;
|
||||||
|
|
||||||
|
onDeleteEmitter: Emitter<IDeleteEvent>;
|
||||||
|
onDelete: IEvent<IDeleteEvent>;
|
||||||
|
onInsertEmitter: Emitter<IInsertEvent>;
|
||||||
|
onInsert: IEvent<IInsertEvent>;
|
||||||
|
onTrimEmitter: Emitter<number>;
|
||||||
|
onTrim: IEvent<number>;
|
||||||
|
|
||||||
|
get(index: number): T | undefined;
|
||||||
|
set(index: number, value: T): void;
|
||||||
|
push(value: T): void;
|
||||||
|
recycle(): T;
|
||||||
|
pop(): T | undefined;
|
||||||
|
splice(start: number, deleteCount: number, ...items: T[]): void;
|
||||||
|
trimStart(count: number): void;
|
||||||
|
shiftElements(start: number, count: number, offset: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||||
* overriding values at the start of the list.
|
* overriding values at the start of the list.
|
||||||
|
|||||||