Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9c38786bb | ||
|
|
039e103ed8 | ||
|
|
c9aabf9cf5 | ||
|
|
dc1fab5d5d | ||
|
|
b7ef9fa25e | ||
|
|
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 | ||
|
|
936442c813 | ||
|
|
dfb65ca66f | ||
|
|
35c91cbf2d | ||
|
|
587856e2ca | ||
|
|
56a7195487 | ||
|
|
4aa04c6b3f | ||
|
|
28eadddb79 | ||
|
|
59d1ec7bf0 | ||
|
|
7c82c1a815 | ||
|
|
04a0b7347c | ||
|
|
5d8da19c44 | ||
|
|
17ad33dc3d | ||
|
|
f76c7aa8ea | ||
|
|
7b66cd026a | ||
|
|
235457ccdd | ||
|
|
eaa3a63ddf | ||
|
|
8883dc17a7 |
@@ -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'
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ docs/
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
/.idea/
|
/.idea/
|
||||||
.env
|
.env
|
||||||
|
mise.toml
|
||||||
build/
|
build/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": [
|
||||||
|
"import",
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "off"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"typeAware": true,
|
||||||
|
"maxWarnings": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"builtin": true
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"addons/*/src/third-party/*.ts",
|
||||||
|
"**/out/*",
|
||||||
|
"**/out-test/*",
|
||||||
|
"**/out-esbuild/*",
|
||||||
|
"**/out-esbuild-test/*",
|
||||||
|
"**/inwasm-sdks/*",
|
||||||
|
"**/typings/*.d.ts",
|
||||||
|
"**/node_modules",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.mjs"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"constructor-super": "error",
|
||||||
|
"for-direction": "error",
|
||||||
|
"getter-return": "error",
|
||||||
|
"no-async-promise-executor": "error",
|
||||||
|
"no-case-declarations": "error",
|
||||||
|
"no-class-assign": "error",
|
||||||
|
"no-compare-neg-zero": "error",
|
||||||
|
"no-cond-assign": "error",
|
||||||
|
"no-const-assign": "error",
|
||||||
|
"no-constant-binary-expression": "error",
|
||||||
|
"no-constant-condition": "error",
|
||||||
|
"no-control-regex": "error",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-delete-var": "error",
|
||||||
|
"no-dupe-class-members": "error",
|
||||||
|
"no-dupe-else-if": "error",
|
||||||
|
"no-dupe-keys": "error",
|
||||||
|
"no-duplicate-case": "error",
|
||||||
|
"no-empty": "error",
|
||||||
|
"no-empty-character-class": "error",
|
||||||
|
"no-empty-pattern": "error",
|
||||||
|
"no-empty-static-block": "error",
|
||||||
|
"no-ex-assign": "error",
|
||||||
|
"no-extra-boolean-cast": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-func-assign": "error",
|
||||||
|
"no-global-assign": "error",
|
||||||
|
"no-import-assign": "error",
|
||||||
|
"no-invalid-regexp": "error",
|
||||||
|
"no-irregular-whitespace": "error",
|
||||||
|
"no-loss-of-precision": "error",
|
||||||
|
"no-misleading-character-class": "error",
|
||||||
|
"no-new-native-nonconstructor": "error",
|
||||||
|
"no-nonoctal-decimal-escape": "error",
|
||||||
|
"no-obj-calls": "error",
|
||||||
|
"no-prototype-builtins": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-regex-spaces": "error",
|
||||||
|
"no-self-assign": "error",
|
||||||
|
"no-setter-return": "error",
|
||||||
|
"no-shadow-restricted-names": "error",
|
||||||
|
"no-sparse-arrays": "error",
|
||||||
|
"no-this-before-super": "error",
|
||||||
|
"no-unassigned-vars": "error",
|
||||||
|
"no-unexpected-multiline": "error",
|
||||||
|
"no-unreachable": "error",
|
||||||
|
"no-unsafe-finally": "error",
|
||||||
|
"no-unsafe-negation": "error",
|
||||||
|
"no-unsafe-optional-chaining": "error",
|
||||||
|
"no-unused-labels": "error",
|
||||||
|
"no-unused-private-class-members": "error",
|
||||||
|
"no-unused-vars": "error",
|
||||||
|
"no-useless-backreference": "error",
|
||||||
|
"no-useless-catch": "error",
|
||||||
|
"no-useless-escape": "error",
|
||||||
|
"no-with": "error",
|
||||||
|
"preserve-caught-error": "error",
|
||||||
|
"require-yield": "error",
|
||||||
|
"use-isnan": "error",
|
||||||
|
"valid-typeof": "error",
|
||||||
|
"no-array-constructor": "error",
|
||||||
|
"no-unused-expressions": "error",
|
||||||
|
"typescript/ban-ts-comment": "error",
|
||||||
|
"typescript/no-duplicate-enum-values": "error",
|
||||||
|
"typescript/no-empty-object-type": "error",
|
||||||
|
"typescript/no-explicit-any": "error",
|
||||||
|
"typescript/no-extra-non-null-assertion": "error",
|
||||||
|
"typescript/no-misused-new": "error",
|
||||||
|
"typescript/no-namespace": "error",
|
||||||
|
"typescript/no-non-null-asserted-optional-chain": "error",
|
||||||
|
"typescript/no-require-imports": "error",
|
||||||
|
"typescript/no-this-alias": "error",
|
||||||
|
"typescript/no-unnecessary-type-constraint": "error",
|
||||||
|
"typescript/no-unsafe-declaration-merging": "error",
|
||||||
|
"typescript/no-unsafe-function-type": "error",
|
||||||
|
"typescript/no-wrapper-object-types": "error",
|
||||||
|
"typescript/prefer-as-const": "error",
|
||||||
|
"typescript/prefer-namespace-keyword": "error",
|
||||||
|
"typescript/triple-slash-reference": "error",
|
||||||
|
"import/no-cycle": "error"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
"**/*.mts",
|
||||||
|
"**/*.cts"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"constructor-super": "off",
|
||||||
|
"getter-return": "off",
|
||||||
|
"no-class-assign": "off",
|
||||||
|
"no-const-assign": "off",
|
||||||
|
"no-dupe-class-members": "off",
|
||||||
|
"no-dupe-keys": "off",
|
||||||
|
"no-func-assign": "off",
|
||||||
|
"no-import-assign": "off",
|
||||||
|
"no-new-native-nonconstructor": "off",
|
||||||
|
"no-obj-calls": "off",
|
||||||
|
"no-redeclare": "off",
|
||||||
|
"no-setter-return": "off",
|
||||||
|
"no-this-before-super": "off",
|
||||||
|
"no-unreachable": "off",
|
||||||
|
"no-unsafe-negation": "off",
|
||||||
|
"no-var": "error",
|
||||||
|
"no-with": "off",
|
||||||
|
"prefer-const": "error",
|
||||||
|
"prefer-rest-params": "error",
|
||||||
|
"prefer-spread": "error"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.ts"
|
||||||
|
],
|
||||||
|
"jsPlugins": [
|
||||||
|
"@stylistic/eslint-plugin",
|
||||||
|
{
|
||||||
|
"name": "jsdoc-js",
|
||||||
|
"specifier": "eslint-plugin-jsdoc"
|
||||||
|
},
|
||||||
|
"oxlint-plugin-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@stylistic/comma-spacing": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"before": false,
|
||||||
|
"after": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/indent": [
|
||||||
|
"warn",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"@stylistic/semi": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"@stylistic/quotes": [
|
||||||
|
"warn",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"allowTemplateLiterals": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/member-delimiter-style": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"multiline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": true
|
||||||
|
},
|
||||||
|
"singleline": {
|
||||||
|
"delimiter": "comma",
|
||||||
|
"requireLast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/type-annotation-spacing": "warn",
|
||||||
|
"@stylistic/space-unary-ops": "warn",
|
||||||
|
"@stylistic/keyword-spacing": "warn",
|
||||||
|
"@stylistic/max-len": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"code": 1000,
|
||||||
|
"comments": 100,
|
||||||
|
"ignoreTrailingComments": true,
|
||||||
|
"ignoreUrls": true,
|
||||||
|
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/new-parens": "warn",
|
||||||
|
"@stylistic/no-extra-semi": "error",
|
||||||
|
"@stylistic/no-trailing-spaces": "warn",
|
||||||
|
"eslint-js/object-curly-spacing": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"@stylistic/spaced-comment": [
|
||||||
|
"warn",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"markers": [
|
||||||
|
"/"
|
||||||
|
],
|
||||||
|
"exceptions": [
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"jsdoc-js/check-alignment": "warn",
|
||||||
|
"jsdoc-js/check-param-names": "warn",
|
||||||
|
"jsdoc-js/multiline-blocks": "warn",
|
||||||
|
"jsdoc-js/no-multi-asterisks": "warn",
|
||||||
|
"eslint-js/one-var": [
|
||||||
|
"warn",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"eslint-js/no-restricted-syntax": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"selector": "CallExpression[callee.name='requestAnimationFrame']",
|
||||||
|
"message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "CallExpression[callee.name='cancelAnimationFrame']",
|
||||||
|
"message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']",
|
||||||
|
"message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']",
|
||||||
|
"message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']",
|
||||||
|
"message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"curly": [
|
||||||
|
"warn",
|
||||||
|
"multi-line"
|
||||||
|
],
|
||||||
|
"eqeqeq": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"no-duplicate-imports": "warn",
|
||||||
|
"no-else-return": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowElseIf": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-eval": "warn",
|
||||||
|
"no-irregular-whitespace": "warn",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
".*\\/out\\/.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-unsafe-finally": "warn",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"no-var": "warn",
|
||||||
|
"no-empty": "off",
|
||||||
|
"no-empty-pattern": "off",
|
||||||
|
"no-cond-assign": "off",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"for-direction": "off",
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"no-self-assign": "off",
|
||||||
|
"no-async-promise-executor": "off",
|
||||||
|
"prefer-rest-params": "off",
|
||||||
|
"no-control-regex": "off",
|
||||||
|
"no-fallthrough": "off",
|
||||||
|
"prefer-spread": "off",
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"no-unused-expressions": "off",
|
||||||
|
"typescript/array-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"default": "array",
|
||||||
|
"readonly": "generic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/consistent-type-assertions": "warn",
|
||||||
|
"typescript/consistent-type-definitions": "warn",
|
||||||
|
"typescript/explicit-function-return-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/explicit-member-accessibility": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"accessibility": "explicit",
|
||||||
|
"overrides": {
|
||||||
|
"constructors": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-confusing-void-expression": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"ignoreArrowShorthand": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-useless-constructor": "warn",
|
||||||
|
"typescript/prefer-nullish-coalescing": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"ignorePrimitives": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/prefer-namespace-keyword": "warn",
|
||||||
|
"typescript/no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"vars": "all",
|
||||||
|
"args": "none"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/no-require-imports": "off",
|
||||||
|
"typescript/no-explicit-any": "off",
|
||||||
|
"typescript/no-wrapper-object-types": "off",
|
||||||
|
"typescript/no-empty-object-type": "off",
|
||||||
|
"typescript/no-unsafe-function-type": "off",
|
||||||
|
"typescript/no-this-alias": "off",
|
||||||
|
"typescript/no-namespace": "off",
|
||||||
|
"typescript/no-duplicate-enum-values": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"**/*.api.ts",
|
||||||
|
"**/*.test.ts"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"eslint-js/object-curly-spacing": "off",
|
||||||
|
"@stylistic/max-len": "off",
|
||||||
|
"typescript/no-unused-vars": "off",
|
||||||
|
"typescript/explicit-function-return-type": "off",
|
||||||
|
"typescript/explicit-member-accessibility": "off"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"demo/**/*.ts",
|
||||||
|
"**/*.benchmark.ts"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"typescript/prefer-nullish-coalescing": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"options": {
|
||||||
|
"maxWarnings": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"builtin": true
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"typings/**/*.d.ts"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"typescript"
|
||||||
|
],
|
||||||
|
"jsPlugins": [
|
||||||
|
"@stylistic/eslint-plugin",
|
||||||
|
{
|
||||||
|
"name": "jsdoc-js",
|
||||||
|
"specifier": "eslint-plugin-jsdoc"
|
||||||
|
},
|
||||||
|
"oxlint-plugin-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@stylistic/indent": [
|
||||||
|
"warn",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"@stylistic/semi": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"@stylistic/quotes": [
|
||||||
|
"warn",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"allowTemplateLiterals": "always"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/member-delimiter-style": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"multiline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": true
|
||||||
|
},
|
||||||
|
"singleline": {
|
||||||
|
"delimiter": "comma",
|
||||||
|
"requireLast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/type-annotation-spacing": "warn",
|
||||||
|
"@stylistic/keyword-spacing": "warn",
|
||||||
|
"@stylistic/max-len": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"code": 1000,
|
||||||
|
"comments": 80,
|
||||||
|
"ignoreUrls": true,
|
||||||
|
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/no-extra-semi": "error",
|
||||||
|
"@stylistic/no-trailing-spaces": "warn",
|
||||||
|
"eslint-js/object-curly-spacing": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"@stylistic/spaced-comment": [
|
||||||
|
"warn",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"markers": [
|
||||||
|
"/"
|
||||||
|
],
|
||||||
|
"exceptions": [
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/comma-dangle": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"objects": "never",
|
||||||
|
"arrays": "never",
|
||||||
|
"functions": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@stylistic/eol-last": "warn",
|
||||||
|
"curly": [
|
||||||
|
"warn",
|
||||||
|
"multi-line"
|
||||||
|
],
|
||||||
|
"eqeqeq": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"jsdoc-js/check-alignment": "warn",
|
||||||
|
"jsdoc-js/check-param-names": "warn",
|
||||||
|
"no-irregular-whitespace": "warn",
|
||||||
|
"typescript/array-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"default": "array",
|
||||||
|
"readonly": "generic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/explicit-function-return-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"typescript/prefer-namespace-keyword": "warn"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -15,9 +15,6 @@
|
|||||||
"--recursive",
|
"--recursive",
|
||||||
"${workspaceRoot}/out/**/*.test.js"
|
"${workspaceRoot}/out/**/*.test.js"
|
||||||
],
|
],
|
||||||
"env": {
|
|
||||||
"NODE_PATH": "${workspaceRoot}/out"
|
|
||||||
},
|
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
|
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"internalConsoleOptions": "openOnSessionStart"
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
**Core Structure**: xterm.js is a multi-target terminal emulator with these main source areas:
|
**Core Structure**: xterm.js is a multi-target terminal emulator with three main distributions:
|
||||||
- `src/primitives/`: Building blocks (buffer, parser, input, data, utilities); TypeScript project `src/primitives/tsconfig.json`
|
- `src/browser/`: Full-featured browser terminal with DOM rendering
|
||||||
- `src/runtime/`: Services, `CoreTerminal`, `InputHandler`, public API adapters; TypeScript project `src/runtime/tsconfig.json`
|
- `src/headless/`: Server-side terminal for Node.js (no DOM)
|
||||||
- `src/target-browser/`: Browser terminal with DOM rendering
|
- `src/common/`: Shared core logic (parsing, buffer management, terminal state)
|
||||||
- `src/target-headless/`: Node terminal without DOM
|
|
||||||
|
|
||||||
See `src/ARCHITECTURE.md` for the dependency graph and import-path conventions (`common/*`, `target-browser/*`, `target-headless/*`).
|
|
||||||
|
|
||||||
**Key Classes**:
|
**Key Classes**:
|
||||||
- `Terminal` (browser/headless): Public API wrapper
|
- `Terminal` (browser/headless): Public API wrapper
|
||||||
@@ -29,6 +26,7 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
|
|||||||
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
|
||||||
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
|
||||||
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
|
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
|
||||||
|
- Lint: `npm run lint` (oxlint with type-aware rules, then ESLint for `naming-convention` only), `npm run lint-api` for `typings/`, `npm run lint-fix` for oxlint auto-fix
|
||||||
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
|
||||||
|
|
||||||
## Addon Development Pattern
|
## Addon Development Pattern
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -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 '';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"../../../node_modules/@types/mocha"
|
"../../../node_modules/@types/mocha"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"@xterm/addon-clipboard": [
|
"@xterm/addon-clipboard": [
|
||||||
"../typings/addon-clipboard.d.ts"
|
"../typings/addon-clipboard.d.ts"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
|
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
|
||||||
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
|
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
|
||||||
import { ViewportConstants } from 'target-browser/shared/Constants';
|
import { ViewportConstants } from 'browser/shared/Constants';
|
||||||
|
|
||||||
interface ITerminalDimensions {
|
interface ITerminalDimensions {
|
||||||
/**
|
/**
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -15,8 +15,8 @@
|
|||||||
"../../../node_modules/@types/mocha"
|
"../../../node_modules/@types/mocha"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"@xterm/addon-fit": [
|
"@xterm/addon-fit": [
|
||||||
"../typings/addon-fit.d.ts"
|
"../typings/addon-fit.d.ts"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -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 |
@@ -3,13 +3,13 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
|
||||||
import { IIPImageStorage } from './IIPImageStorage';
|
import { IIPImageStorage } from './IIPImageStorage';
|
||||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||||
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
||||||
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
||||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
import { imageType, UNSUPPORTED_TYPE } from './Metrics';
|
||||||
|
import { Drawable } from './Primitives';
|
||||||
|
|
||||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||||
const enum DecoderConst {
|
const enum DecoderConst {
|
||||||
@@ -39,13 +39,11 @@ 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;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly _opts: IImageAddonOptions,
|
private readonly _opts: IImageAddonOptions,
|
||||||
private readonly _renderer: ImageRenderer,
|
|
||||||
private readonly _storage: IIPImageStorage,
|
private readonly _storage: IIPImageStorage,
|
||||||
private readonly _coreTerminal: ITerminalExt
|
private readonly _coreTerminal: ITerminalExt
|
||||||
) {
|
) {
|
||||||
@@ -63,7 +61,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 +117,16 @@ 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;
|
// IMPORTANT: ReportCellSize uses logical points (CSS pixels)
|
||||||
let height = CELL_SIZE_DEFAULT.height;
|
let w = CELL_SIZE_DEFAULT.width;
|
||||||
if (this._renderer.dimensions) {
|
let h = CELL_SIZE_DEFAULT.height;
|
||||||
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
const dimensions = this._coreTerminal.dimensions;
|
||||||
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
if (dimensions) {
|
||||||
|
w = dimensions.css.canvas.width / this._coreTerminal.cols;
|
||||||
|
h = 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;
|
||||||
}
|
}
|
||||||
@@ -149,22 +148,21 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
|
|
||||||
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
||||||
|
|
||||||
let w = 0;
|
|
||||||
let h = 0;
|
|
||||||
|
|
||||||
// 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;
|
if (!(cond = metrics.width && metrics.height && metrics.width * metrics.height < this._opts.pixelLimit)) {
|
||||||
h = this._metrics.height;
|
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
|
||||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
|
||||||
[w, h] = this._resize(w, h).map(Math.floor);
|
|
||||||
cond = w && h && w * h < this._opts.pixelLimit;
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('IIP: unsupported image type');
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('IIP: error during BASE64 decoding');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
@@ -172,42 +170,52 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let blob: Blob | ImageData;
|
let bmSrc: Blob | ImageData;
|
||||||
if (this._metrics.mime === 'image/qoi') {
|
let imgBlob: Blob;
|
||||||
|
if (metrics.mime === 'image/qoi') {
|
||||||
const data = this._qoiDec.decode(this._dec.data8);
|
const data = this._qoiDec.decode(this._dec.data8);
|
||||||
blob = new ImageData(
|
bmSrc = new ImageData(
|
||||||
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
||||||
this._qoiDec.width,
|
this._qoiDec.width,
|
||||||
this._qoiDec.height
|
this._qoiDec.height
|
||||||
);
|
);
|
||||||
this._qoiDec.release();
|
this._qoiDec.release();
|
||||||
if (w === this._qoiDec.width && h === this._qoiDec.height) {
|
|
||||||
// use fast-path if we don't need to rescale
|
|
||||||
this._dec.release();
|
|
||||||
const canvas = ImageRenderer.createCanvas(undefined, this._qoiDec.width, this._qoiDec.height);
|
|
||||||
canvas.getContext('2d')?.putImageData(blob, 0, 0);
|
|
||||||
this._storage.addImage(canvas);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
imgBlob = bmSrc = new Blob([this._dec.data8], { type: metrics.mime });
|
||||||
}
|
}
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
return createImageBitmap(bmSrc)
|
||||||
.then(bm => {
|
.then(bm => {
|
||||||
this._storage.addImage(bm);
|
const [w, h] = this._resize(metrics.width, metrics.height);
|
||||||
|
this._storage.addImage(new Drawable(bm), imgBlob, metrics, w / metrics.width, h / metrics.height);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _resize(w: number, h: number): [number, number] {
|
private _resize(w: number, h: number): [number, number] {
|
||||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
let cw;
|
||||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
let ch;
|
||||||
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
|
let width;
|
||||||
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
|
let height;
|
||||||
|
const dimensions = this._coreTerminal.dimensions;
|
||||||
|
if (dimensions) {
|
||||||
|
width = dimensions.device.canvas.width;
|
||||||
|
height = dimensions.device.canvas.height;
|
||||||
|
cw = width / this._coreTerminal.cols;
|
||||||
|
ch = height / this._coreTerminal.rows;
|
||||||
|
} else {
|
||||||
|
cw = CELL_SIZE_DEFAULT.width;
|
||||||
|
ch = CELL_SIZE_DEFAULT.height;
|
||||||
|
width = cw * this._coreTerminal.cols;
|
||||||
|
height = ch * this._coreTerminal.rows;
|
||||||
|
}
|
||||||
|
|
||||||
const rw = this._dim(this._header.width!, width, cw);
|
const rw = this._dim(this._header.width ?? '', width, cw);
|
||||||
const rh = this._dim(this._header.height!, height, ch);
|
const rh = this._dim(this._header.height ?? '', height, ch);
|
||||||
if (!rw && !rh) {
|
if (!rw && !rh) {
|
||||||
const wf = width / w; // TODO: should this respect initial cursor offset?
|
const wf = width / w; // TODO: should this respect initial cursor offset?
|
||||||
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
||||||
@@ -222,8 +230,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IAddImageOpts } from 'Types';
|
import { IAddImageOpts, IDrawable, IMetrics } from './Types';
|
||||||
import { ImageStorage } from './ImageStorage';
|
import { ImageStorage } from './ImageStorage';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +22,9 @@ export class IIPImageStorage {
|
|||||||
* Add an IIP image to storage.
|
* Add an IIP image to storage.
|
||||||
* Always uses scrolling mode — cursor advances past the image.
|
* Always uses scrolling mode — cursor advances past the image.
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, prescaleX: number, prescaleY: number): void {
|
||||||
this._storage.addImage(img, this._addImageOpts);
|
this._addImageOpts.prescaleX = prescaleX;
|
||||||
|
this._addImageOpts.prescaleY = prescaleY;
|
||||||
|
this._storage.addImage(src, data, metrics, this._addImageOpts);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
|
|
||||||
|
|
||||||
export interface IMetrics {
|
|
||||||
mime: ImageType;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UNSUPPORTED_TYPE: IMetrics = {
|
|
||||||
mime: 'unsupported',
|
|
||||||
width: 0,
|
|
||||||
height: 0
|
|
||||||
};
|
|
||||||
|
|
||||||
export function imageType(d: Uint8Array): IMetrics {
|
|
||||||
if (d.length < 24) {
|
|
||||||
return UNSUPPORTED_TYPE;
|
|
||||||
}
|
|
||||||
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
|
|
||||||
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
|
||||||
// + first chunk must be IHDR
|
|
||||||
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
|
||||||
return {
|
|
||||||
mime: 'image/png',
|
|
||||||
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
|
||||||
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// JPEG: FF D8 FF
|
|
||||||
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
|
|
||||||
const [width, height] = jpgSize(d);
|
|
||||||
return { mime: 'image/jpeg', width, height };
|
|
||||||
}
|
|
||||||
// GIF: GIF87a or GIF89a
|
|
||||||
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
|
||||||
return {
|
|
||||||
mime: 'image/gif',
|
|
||||||
width: d[7] << 8 | d[6],
|
|
||||||
height: d[9] << 8 | d[8]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// QOI: qoif
|
|
||||||
if (d32[0] === 0x66696F71) {
|
|
||||||
return {
|
|
||||||
mime: 'image/qoi',
|
|
||||||
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
|
||||||
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return UNSUPPORTED_TYPE;
|
|
||||||
}
|
|
||||||
|
|
||||||
function jpgSize(d: Uint8Array): [number, number] {
|
|
||||||
const len = d.length;
|
|
||||||
let i = 4;
|
|
||||||
let blockLength = d[i] << 8 | d[i + 1];
|
|
||||||
while (true) {
|
|
||||||
i += blockLength;
|
|
||||||
if (i >= len) {
|
|
||||||
// exhausted without size info
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
if (d[i] !== 0xFF) {
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
|
|
||||||
if (i + 8 < len) {
|
|
||||||
return [
|
|
||||||
d[i + 7] << 8 | d[i + 8],
|
|
||||||
d[i + 5] << 8 | d[i + 6]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [0, 0];
|
|
||||||
}
|
|
||||||
i += 2;
|
|
||||||
blockLength = d[i] << 8 | d[i + 1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -144,6 +144,9 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
terminal.options.windowOptions = windowOps;
|
terminal.options.windowOptions = windowOps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apply placeholder setting
|
||||||
|
this._renderer.showPlaceholder(this._opts.showPlaceholder);
|
||||||
|
|
||||||
this._disposeLater(
|
this._disposeLater(
|
||||||
this._renderer,
|
this._renderer,
|
||||||
this._storage,
|
this._storage,
|
||||||
@@ -187,7 +190,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
// iTerm IIP handler
|
// iTerm IIP handler
|
||||||
if (this._opts.iipSupport) {
|
if (this._opts.iipSupport) {
|
||||||
const iipStorage = new IIPImageStorage(this._storage!);
|
const iipStorage = new IIPImageStorage(this._storage!);
|
||||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
const iipHandler = new IIPHandler(this._opts, iipStorage, terminal);
|
||||||
this._handlers.set('iip', iipHandler);
|
this._handlers.set('iip', iipHandler);
|
||||||
this._disposeLater(
|
this._disposeLater(
|
||||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||||
@@ -250,10 +253,6 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
return this._storage?.getImageAtBufferCell(x, y);
|
return this._storage?.getImageAtBufferCell(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
||||||
return this._storage?.extractTileAtBufferCell(x, y);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _report(s: string): void {
|
private _report(s: string): void {
|
||||||
this._terminal?._core.input(s, false);
|
this._terminal?._core.input(s, false);
|
||||||
}
|
}
|
||||||
@@ -344,8 +343,8 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
|||||||
switch (params[1]) {
|
switch (params[1]) {
|
||||||
// we only implement read and read_max here
|
// we only implement read and read_max here
|
||||||
case GaAction.READ:
|
case GaAction.READ:
|
||||||
let width = this._renderer?.dimensions?.css.canvas.width;
|
let width = this._renderer?.dimensions?.device.canvas.width;
|
||||||
let height = this._renderer?.dimensions?.css.canvas.height;
|
let height = this._renderer?.dimensions?.device.canvas.height;
|
||||||
if (!width || !height) {
|
if (!width || !height) {
|
||||||
// for some reason we have no working image renderer
|
// for some reason we have no working image renderer
|
||||||
// --> fallback to default cell size
|
// --> fallback to default cell size
|
||||||
|
|||||||
@@ -3,19 +3,14 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
|
||||||
import { IDisposable } from '@xterm/xterm';
|
import { IDisposable } from '@xterm/xterm';
|
||||||
import { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
import type { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||||
|
import { createCanvas } from './Primitives';
|
||||||
|
|
||||||
const enum Constants {
|
|
||||||
PLACEHOLDER_LENGTH = 4096,
|
|
||||||
PLACEHOLDER_HEIGHT = 24
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ImageRenderer - terminal frontend extension:
|
* ImageRenderer - terminal frontend extension:
|
||||||
* - provide primitives for canvas, ImageData, Bitmap (static)
|
|
||||||
* - add canvas layer to DOM (browser only for now)
|
* - add canvas layer to DOM (browser only for now)
|
||||||
* - draw image tiles onRender
|
* - draw image tiles onRender
|
||||||
*/
|
*/
|
||||||
@@ -24,51 +19,14 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
|
||||||
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
|
||||||
private _placeholder: HTMLCanvasElement | undefined;
|
private _placeholder: HTMLCanvasElement | undefined;
|
||||||
private _placeholderBitmap: ImageBitmap | undefined;
|
|
||||||
private _optionsRefresh = this._register(new MutableDisposable());
|
private _optionsRefresh = this._register(new MutableDisposable());
|
||||||
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
||||||
private _renderService: IRenderService | undefined;
|
private _renderService: IRenderService | undefined;
|
||||||
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
||||||
|
|
||||||
// drawing primitive - canvas
|
// some local variables for faster access
|
||||||
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
|
private _dimensions: IRenderDimensions | undefined;
|
||||||
/**
|
private _cellSize: ICellSize | undefined;
|
||||||
* NOTE: We normally dont care, from which document the canvas
|
|
||||||
* gets created, so we can fall back to global document,
|
|
||||||
* if the terminal has no document associated yet.
|
|
||||||
* This way early image loads before calling .open keep working
|
|
||||||
* (still discouraged though, as the metrics will be screwed up).
|
|
||||||
* Only the DOM output canvas should be on the terminal's document,
|
|
||||||
* which gets explicitly checked in `insertLayerToDom`.
|
|
||||||
*/
|
|
||||||
const canvas = (localDocument ?? document).createElement('canvas');
|
|
||||||
canvas.width = width | 0;
|
|
||||||
canvas.height = height | 0;
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawing primitive - ImageData with optional buffer
|
|
||||||
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
|
|
||||||
if (typeof ImageData !== 'function') {
|
|
||||||
const imgData = ctx.createImageData(width, height);
|
|
||||||
if (buffer) {
|
|
||||||
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
|
|
||||||
}
|
|
||||||
return imgData;
|
|
||||||
}
|
|
||||||
return buffer
|
|
||||||
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
|
|
||||||
: new ImageData(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawing primitive - ImageBitmap
|
|
||||||
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
|
|
||||||
if (typeof createImageBitmap !== 'function') {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
return createImageBitmap(img);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
constructor(private _terminal: ITerminalExt) {
|
constructor(private _terminal: ITerminalExt) {
|
||||||
super();
|
super();
|
||||||
@@ -100,8 +58,6 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
}
|
}
|
||||||
this._renderService = undefined;
|
this._renderService = undefined;
|
||||||
this._layers.clear();
|
this._layers.clear();
|
||||||
this._placeholderBitmap?.close();
|
|
||||||
this._placeholderBitmap = undefined;
|
|
||||||
this._placeholder = undefined;
|
this._placeholder = undefined;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -110,15 +66,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
* Enable the placeholder.
|
* Enable the placeholder.
|
||||||
*/
|
*/
|
||||||
public showPlaceholder(value: boolean): void {
|
public showPlaceholder(value: boolean): void {
|
||||||
if (value) {
|
this._placeholder = value ? this._createPlaceHolder() : undefined;
|
||||||
if (!this._placeholder && this.cellSize.height !== -1) {
|
|
||||||
this._createPlaceHolder(Math.max(this.cellSize.height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._placeholderBitmap?.close();
|
|
||||||
this._placeholderBitmap = undefined;
|
|
||||||
this._placeholder = undefined;
|
|
||||||
}
|
|
||||||
this._renderService?.refreshRows(0, this._terminal.rows);
|
this._renderService?.refreshRows(0, this._terminal.rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,22 +79,27 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current cell size (float).
|
* Current cell size.
|
||||||
*/
|
*/
|
||||||
public get cellSize(): ICellSize {
|
public getCellSize(dimensions?: IRenderDimensions): ICellSize | undefined {
|
||||||
return {
|
dimensions ??= this._terminal.dimensions;
|
||||||
width: this.dimensions?.css.cell.width || -1,
|
if (dimensions) {
|
||||||
height: this.dimensions?.css.cell.height || -1
|
return {
|
||||||
};
|
width: dimensions.device.canvas.width / this._terminal.cols,
|
||||||
|
height: dimensions.device.canvas.height / this._terminal.rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear a region of the image layer canvas.
|
* Clear a region of the image layer canvas.
|
||||||
*/
|
*/
|
||||||
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
||||||
const y = start * (this.dimensions?.css.cell.height || 0);
|
const deviceGrid = this._cellSize;
|
||||||
const w = this.dimensions?.css.canvas.width || 0;
|
if (!deviceGrid || !this._dimensions) return;
|
||||||
const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
|
const y = Math.floor(start * deviceGrid.height);
|
||||||
|
const w = this._dimensions.device.canvas.width;
|
||||||
|
const h = Math.ceil((end + 1 - start) * deviceGrid.height);
|
||||||
if (!layer || layer === 'top') {
|
if (!layer || layer === 'top') {
|
||||||
this._layers.get('top')?.clearRect(0, y, w, h);
|
this._layers.get('top')?.clearRect(0, y, w, h);
|
||||||
}
|
}
|
||||||
@@ -174,142 +127,103 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
*/
|
*/
|
||||||
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
||||||
const ctx = this._layers.get(imgSpec.layer);
|
const ctx = this._layers.get(imgSpec.layer);
|
||||||
if (!ctx) {
|
const initialGrid = imgSpec.cellSize;
|
||||||
return;
|
const deviceGrid = this._cellSize;
|
||||||
}
|
|
||||||
const { width, height } = this.cellSize;
|
|
||||||
|
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
if (!ctx || !imgSpec.src.native || !deviceGrid) {
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._rescaleImage(imgSpec, width, height);
|
const img = imgSpec.src;
|
||||||
const img = imgSpec.actual!;
|
const cols = Math.ceil(img.width * imgSpec.prescaleX / initialGrid.width);
|
||||||
const cols = Math.ceil(img.width / width);
|
|
||||||
|
|
||||||
const sx = (tileId % cols) * width;
|
const gridWidth = initialGrid.width / imgSpec.prescaleX;
|
||||||
const sy = Math.floor(tileId / cols) * height;
|
const gridHeight = initialGrid.height / imgSpec.prescaleY;
|
||||||
const dx = col * width;
|
|
||||||
const dy = row * height;
|
|
||||||
|
|
||||||
// safari bug: never access image source out of bounds
|
// crops are initialGrid px
|
||||||
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
|
const sx = (tileId % cols) * gridWidth + imgSpec.precropX;
|
||||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
const sy = Math.floor(tileId / cols) * gridHeight + imgSpec.precropY;
|
||||||
|
const dx = Math.floor(col * deviceGrid.width + imgSpec.offsetX);
|
||||||
|
const dy = Math.floor(row * deviceGrid.height + imgSpec.offsetY);
|
||||||
|
|
||||||
|
// safari bug: never access image source out of bounds, thus we clamp its dimensions
|
||||||
|
const sWidth = Math.min(count * gridWidth, img.width - sx);
|
||||||
|
const sHeight = Math.min(gridHeight, img.height - sy);
|
||||||
|
const dWidth = Math.ceil(sWidth / gridWidth * deviceGrid.width * imgSpec.scaleX);
|
||||||
|
const dHeight = Math.ceil(sHeight / gridHeight * deviceGrid.height * imgSpec.scaleY);
|
||||||
|
|
||||||
|
if (imgSpec.blendMode === 'overwrite') {
|
||||||
|
ctx.clearRect(dx, dy, dWidth, dHeight);
|
||||||
|
}
|
||||||
|
|
||||||
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
||||||
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
||||||
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
||||||
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
||||||
ctx.drawImage(
|
ctx.drawImage(
|
||||||
img,
|
img.native,
|
||||||
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
Math.floor(sx), Math.floor(sy), Math.ceil(sWidth), Math.ceil(sHeight),
|
||||||
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
dx, dy, dWidth, dHeight
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a single tile from an image.
|
|
||||||
*/
|
|
||||||
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
|
|
||||||
const { width, height } = this.cellSize;
|
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._rescaleImage(imgSpec, width, height);
|
|
||||||
const img = imgSpec.actual!;
|
|
||||||
const cols = Math.ceil(img.width / width);
|
|
||||||
const sx = (tileId % cols) * width;
|
|
||||||
const sy = Math.floor(tileId / cols) * height;
|
|
||||||
const finalWidth = width + sx > img.width ? img.width - sx : width;
|
|
||||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
|
||||||
|
|
||||||
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(
|
|
||||||
img,
|
|
||||||
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
|
|
||||||
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
|
|
||||||
);
|
|
||||||
return canvas;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw a line with placeholder on the image layer canvas.
|
* Draw a line with placeholder on the image layer canvas.
|
||||||
*/
|
*/
|
||||||
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
||||||
const ctx = this._layers.get('top');
|
const ctx = this._layers.get('top');
|
||||||
if (ctx) {
|
const deviceGrid = this._cellSize;
|
||||||
const { width, height } = this.cellSize;
|
if (!ctx || !deviceGrid) {
|
||||||
|
return;
|
||||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
|
||||||
if (width === -1 || height === -1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._placeholder) {
|
|
||||||
this._createPlaceHolder(Math.max(height + 1, Constants.PLACEHOLDER_HEIGHT));
|
|
||||||
} else if (height >= this._placeholder!.height) {
|
|
||||||
this._createPlaceHolder(height + 1);
|
|
||||||
}
|
|
||||||
if (!this._placeholder) return;
|
|
||||||
ctx.drawImage(
|
|
||||||
this._placeholderBitmap ?? this._placeholder!,
|
|
||||||
col * width,
|
|
||||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
|
||||||
width * count,
|
|
||||||
height,
|
|
||||||
col * width,
|
|
||||||
row * height,
|
|
||||||
width * count,
|
|
||||||
height
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
this._placeholder ??= this._createPlaceHolder();
|
||||||
|
if (!this._placeholder) return;
|
||||||
|
ctx.drawImage(
|
||||||
|
this._placeholder,
|
||||||
|
0, 0, 1, 1,
|
||||||
|
col * deviceGrid.width,
|
||||||
|
row * deviceGrid.height,
|
||||||
|
count * deviceGrid.width,
|
||||||
|
deviceGrid.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rescale image layer canvas if needed.
|
* Rescale image layer canvas if needed.
|
||||||
* Checked once from `ImageStorage.render`.
|
* Checked once from `ImageStorage.render`.
|
||||||
|
* NOTE: This method updates dimensions on instance properties
|
||||||
|
* for faster access during draw and clear calls.
|
||||||
|
* So make sure to always call this before doing any draws.
|
||||||
*/
|
*/
|
||||||
public rescaleCanvas(): void {
|
public rescaleCanvas(force: boolean = false): void {
|
||||||
const w = this.dimensions?.css.canvas.width || 0;
|
const dimensions = this._terminal.dimensions;
|
||||||
const h = this.dimensions?.css.canvas.height || 0;
|
if (dimensions) {
|
||||||
for (const ctx of this._layers.values()) {
|
const cssW = dimensions.css.canvas.width;
|
||||||
if (ctx.canvas.width !== w || ctx.canvas.height !== h) {
|
const cssH = dimensions.css.canvas.height;
|
||||||
ctx.canvas.width = w;
|
const devW = dimensions.device.canvas.width;
|
||||||
ctx.canvas.height = h;
|
const devH = dimensions.device.canvas.height;
|
||||||
|
let recalc = force;
|
||||||
|
for (const ctx of this._layers.values()) {
|
||||||
|
if (ctx.canvas.width !== devW
|
||||||
|
|| ctx.canvas.height !== devH
|
||||||
|
|| ctx.canvas.style.width !== `${cssW}px`
|
||||||
|
|| ctx.canvas.style.height !== `${cssH}px`
|
||||||
|
) {
|
||||||
|
ctx.canvas.width = devW;
|
||||||
|
ctx.canvas.height = devH;
|
||||||
|
ctx.canvas.style.width = `${cssW}px`;
|
||||||
|
ctx.canvas.style.height = `${cssH}px`;
|
||||||
|
recalc = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if (recalc) {
|
||||||
}
|
this._dimensions = dimensions;
|
||||||
|
this._cellSize = this.getCellSize(dimensions);
|
||||||
/**
|
}
|
||||||
* Rescale image in storage if needed.
|
} else {
|
||||||
*/
|
// we were not able to get render dimensions:
|
||||||
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
|
// set cellsize to undefined so we don't try to draw anything
|
||||||
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
|
this._cellSize = this.getCellSize();
|
||||||
return;
|
|
||||||
}
|
|
||||||
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
|
|
||||||
if (currentWidth === originalWidth && currentHeight === originalHeight) {
|
|
||||||
spec.actual = spec.orig;
|
|
||||||
spec.actualCellSize.width = originalWidth;
|
|
||||||
spec.actualCellSize.height = originalHeight;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const canvas = ImageRenderer.createCanvas(
|
|
||||||
this.document,
|
|
||||||
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
|
|
||||||
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
|
|
||||||
);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
if (ctx) {
|
|
||||||
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
|
|
||||||
spec.actual = canvas;
|
|
||||||
spec.actualCellSize.width = currentWidth;
|
|
||||||
spec.actualCellSize.height = currentHeight;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,9 +250,12 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
if (this._layers.has(layer)) {
|
if (this._layers.has(layer)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const canvas = ImageRenderer.createCanvas(
|
const canvas = createCanvas(
|
||||||
this.document, this.dimensions?.css.canvas.width || 0,
|
this.document,
|
||||||
this.dimensions?.css.canvas.height || 0
|
this.dimensions?.device.canvas.width || 0,
|
||||||
|
this.dimensions?.device.canvas.height || 0,
|
||||||
|
this.dimensions?.css.canvas.width || 0,
|
||||||
|
this.dimensions?.css.canvas.height || 0,
|
||||||
);
|
);
|
||||||
canvas.classList.add(`xterm-image-layer-${layer}`);
|
canvas.classList.add(`xterm-image-layer-${layer}`);
|
||||||
const screenElement = this._terminal._core.screenElement;
|
const screenElement = this._terminal._core.screenElement;
|
||||||
@@ -366,6 +283,8 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._layers.set(layer, ctx);
|
this._layers.set(layer, ctx);
|
||||||
|
// force rescaling to update stored cellSize (maybe not be populated yet)
|
||||||
|
this.rescaleCanvas(true);
|
||||||
this.clearAll(layer);
|
this.clearAll(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -381,41 +300,17 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
|||||||
return this._layers.has(layer);
|
return this._layers.has(layer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createPlaceHolder(height: number = Constants.PLACEHOLDER_HEIGHT): void {
|
/**
|
||||||
this._placeholderBitmap?.close();
|
* Create a semi-transparent gray 1x1 placeholder
|
||||||
this._placeholderBitmap = undefined;
|
*/
|
||||||
|
private _createPlaceHolder(): HTMLCanvasElement | undefined {
|
||||||
// create blueprint to fill placeholder with
|
const imgData = new ImageData(1, 1);
|
||||||
const bWidth = 32; // must be 2^n
|
new Uint32Array(imgData.data.buffer).fill(128 << 24 | 128 << 16 | 128 << 8 | 128);
|
||||||
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
|
const canvas = createCanvas(this.document, 1, 1);
|
||||||
const ctx = blueprint.getContext('2d', { alpha: false });
|
const ctx = canvas.getContext('2d', { alpha: true });
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
|
|
||||||
const d32 = new Uint32Array(imgData.data.buffer);
|
|
||||||
const black = toRGBA8888(0, 0, 0);
|
|
||||||
const white = toRGBA8888(255, 255, 255);
|
|
||||||
d32.fill(black);
|
|
||||||
for (let y = 0; y < height; ++y) {
|
|
||||||
const shift = y % 2;
|
|
||||||
const offset = y * bWidth;
|
|
||||||
for (let x = 0; x < bWidth; x += 2) {
|
|
||||||
d32[offset + x + shift] = white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.putImageData(imgData, 0, 0);
|
ctx.putImageData(imgData, 0, 0);
|
||||||
|
return canvas;
|
||||||
// create placeholder line, width aligned to blueprint width
|
|
||||||
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || Constants.PLACEHOLDER_LENGTH;
|
|
||||||
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
|
||||||
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
|
||||||
if (!ctx2) {
|
|
||||||
this._placeholder = undefined;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < width; i += bWidth) {
|
|
||||||
ctx2.drawImage(blueprint, i, 0);
|
|
||||||
}
|
|
||||||
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get document(): Document | undefined {
|
public get document(): Document | undefined {
|
||||||
|
|||||||
@@ -5,12 +5,14 @@
|
|||||||
|
|
||||||
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,
|
IDrawable,
|
||||||
UnderlineStyle, IAddImageOpts
|
IMetrics
|
||||||
} from './Types';
|
} from './Types';
|
||||||
|
import type { IBufferLine } from 'common/buffer/Types';
|
||||||
|
import { CellData } from 'common/buffer/CellData';
|
||||||
|
import { createCanvas } from './Primitives';
|
||||||
|
|
||||||
// fallback default cell size
|
// fallback default cell size
|
||||||
export const CELL_SIZE_DEFAULT: ICellSize = {
|
export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||||
@@ -18,93 +20,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:
|
||||||
@@ -125,7 +46,8 @@ export class ImageStorage implements IDisposable {
|
|||||||
// whether render should do a full clear
|
// whether render should do a full clear
|
||||||
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 _byteLimit: number = 10000000;
|
||||||
|
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;
|
||||||
@@ -164,43 +86,54 @@ export class ImageStorage implements IDisposable {
|
|||||||
this._renderer.clearAll();
|
this._renderer.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: below we need to introduce separation of:
|
||||||
|
* - RAM usage
|
||||||
|
* - VRAM usage
|
||||||
|
* - Blob unsage
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
public getLimit(): number {
|
public getLimit(): number {
|
||||||
return this._pixelLimit * 4 / 1000000;
|
return this._byteLimit / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setLimit(value: number): void {
|
public setLimit(value: number): void {
|
||||||
if (value < 0.5 || value > 1000) {
|
if (value < 0.5 || value > 1000) {
|
||||||
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
|
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1GB');
|
||||||
}
|
}
|
||||||
this._pixelLimit = (value / 4 * 1000000) >>> 0;
|
this._byteLimit = (value * 1000000) >>> 0;
|
||||||
this._evictOldest(0);
|
this._evictOldest(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUsage(): number {
|
public getUsage(): number {
|
||||||
return this._getStoredPixels() * 4 / 1000000;
|
return this._getVRAMUsage() / 1000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _getStoredPixels(): number {
|
private _getVRAMUsage(): number {
|
||||||
let storedPixels = 0;
|
let bytes = 0;
|
||||||
for (const spec of this._images.values()) {
|
for (const spec of this._images.values()) {
|
||||||
if (spec.orig) {
|
bytes += spec.src.bytes;
|
||||||
storedPixels += spec.orig.width * spec.orig.height;
|
}
|
||||||
if (spec.actual && spec.actual !== spec.orig) {
|
return bytes;
|
||||||
storedPixels += spec.actual.width * spec.actual.height;
|
}
|
||||||
}
|
|
||||||
|
public getBlobUsage(): number {
|
||||||
|
let size = 0;
|
||||||
|
for (const spec of this._images.values()) {
|
||||||
|
if (spec.data instanceof Blob) {
|
||||||
|
size += spec.data.size;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return storedPixels;
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _delImg(id: number): void {
|
private _delImg(id: number): void {
|
||||||
const spec = this._images.get(id);
|
const spec = this._images.get(id);
|
||||||
if (!spec) return;
|
if (!spec) return;
|
||||||
this._images.delete(id);
|
this._images.delete(id);
|
||||||
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
spec.src.close();
|
||||||
if (window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
spec.data = undefined;
|
||||||
spec.orig.close();
|
|
||||||
}
|
|
||||||
this.onImageDeleted?.(id);
|
this.onImageDeleted?.(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,8 +170,10 @@ export class ImageStorage implements IDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to add an image to the storage.
|
* Method to add an drawable to the storage.
|
||||||
* @param img - The image to add (canvas or bitmap).
|
* @param src - The drawable to add.
|
||||||
|
* @param data - Blob or Uint8Array of the original image data.
|
||||||
|
* @param metrics - IMetrics of the original image.
|
||||||
* @param opts - Options for addImage:
|
* @param opts - Options for addImage:
|
||||||
* - scrolling: When true, cursor advances with the image.
|
* - scrolling: When true, cursor advances with the image.
|
||||||
* When false, image is placed at ORIGIN and cursor does not move.
|
* When false, image is placed at ORIGIN and cursor does not move.
|
||||||
@@ -247,17 +182,16 @@ export class ImageStorage implements IDisposable {
|
|||||||
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
||||||
* @returns The internal image ID assigned to the stored image.
|
* @returns The internal image ID assigned to the stored image.
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, opts: IAddImageOpts): number {
|
||||||
// never allow storage to exceed memory limit
|
// never allow storage to exceed memory limit
|
||||||
this._evictOldest(img.width * img.height);
|
this._evictOldest(src.bytes);
|
||||||
|
|
||||||
// calc rows x cols needed to display the image
|
// calc rows x cols needed to display the image
|
||||||
let cellSize = this._renderer.cellSize;
|
const prescaleX = opts.prescaleX ?? 1.0;
|
||||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
const prescaleY = opts.prescaleY ?? 1.0;
|
||||||
cellSize = CELL_SIZE_DEFAULT;
|
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
|
||||||
}
|
const cols = Math.ceil(src.width * prescaleX / cellSize.width);
|
||||||
const cols = Math.ceil(img.width / cellSize.width);
|
const rows = Math.ceil(src.height * prescaleY / cellSize.height);
|
||||||
const rows = Math.ceil(img.height / cellSize.height);
|
|
||||||
|
|
||||||
const imageId = ++this._lastId;
|
const imageId = ++this._lastId;
|
||||||
|
|
||||||
@@ -277,10 +211,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) {
|
||||||
@@ -334,15 +268,24 @@ export class ImageStorage implements IDisposable {
|
|||||||
|
|
||||||
// create storage entry
|
// create storage entry
|
||||||
const imgSpec: IImageSpec = {
|
const imgSpec: IImageSpec = {
|
||||||
orig: img,
|
src,
|
||||||
origCellSize: cellSize,
|
data,
|
||||||
actual: img,
|
metrics,
|
||||||
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
cellSize,
|
||||||
marker: endMarker || undefined,
|
marker: endMarker || undefined,
|
||||||
tileCount,
|
tileCount,
|
||||||
bufferType: this._terminal.buffer.active.type,
|
bufferType: this._terminal.buffer.active.type,
|
||||||
layer: opts.layer,
|
layer: opts.layer,
|
||||||
zIndex: opts.zIndex
|
zIndex: opts.zIndex,
|
||||||
|
prescaleX,
|
||||||
|
prescaleY,
|
||||||
|
precropX: 0,
|
||||||
|
precropY: 0,
|
||||||
|
scaleX: 1.0,
|
||||||
|
scaleY: 1.0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
blendMode: 'alpha'
|
||||||
};
|
};
|
||||||
|
|
||||||
// finally add the image
|
// finally add the image
|
||||||
@@ -427,55 +370,42 @@ 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) {
|
|
||||||
if (imgSpec.actual) {
|
|
||||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
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 +443,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;
|
||||||
@@ -525,14 +455,14 @@ export class ImageStorage implements IDisposable {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// found an image tile at oldCol, check if it qualifies for right exapansion
|
// found an image tile at oldCol, check if it qualifies for right exapansion
|
||||||
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
|
const tilesPerRow = Math.ceil(imgSpec.src.width * imgSpec.prescaleX / imgSpec.cellSize.width);
|
||||||
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// 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 +474,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,34 +488,17 @@ 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 src = this._images.get(e.imageId)?.src;
|
||||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
if (src?.native instanceof ImageBitmap || src?.native instanceof VideoFrame) {
|
||||||
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
const canvas = createCanvas(window.document, src.width, src.height);
|
||||||
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
|
canvas.getContext('2d')?.drawImage(src.native, 0, 0, src.width, src.height);
|
||||||
return canvas;
|
return canvas;
|
||||||
}
|
}
|
||||||
return orig as HTMLCanvasElement;
|
return src?.native;
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract active single tile at buffer position.
|
|
||||||
*/
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
|
||||||
const buffer = this._terminal._core.buffer;
|
|
||||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
|
||||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
|
||||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS;
|
|
||||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
|
||||||
const spec = this._images.get(e.imageId);
|
|
||||||
if (spec) {
|
|
||||||
return this._renderer.extractTile(spec, e.tileId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -593,15 +506,12 @@ export class ImageStorage implements IDisposable {
|
|||||||
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
||||||
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
||||||
private _evictOldest(room: number): number {
|
private _evictOldest(room: number): number {
|
||||||
const used = this._getStoredPixels();
|
const used = this._getVRAMUsage();
|
||||||
let current = used;
|
let current = used;
|
||||||
while (this._pixelLimit < current + room && this._images.size) {
|
while (this._byteLimit < current + room && this._images.size) {
|
||||||
const spec = this._images.get(++this._lowestId);
|
const spec = this._images.get(++this._lowestId);
|
||||||
if (spec && spec.orig) {
|
if (spec) {
|
||||||
current -= spec.orig.width * spec.orig.height;
|
current -= spec.src.bytes;
|
||||||
if (spec.actual && spec.orig !== spec.actual) {
|
|
||||||
current -= spec.actual.width * spec.actual.height;
|
|
||||||
}
|
|
||||||
spec.marker?.dispose();
|
spec.marker?.dispose();
|
||||||
this._delImg(this._lowestId);
|
this._delImg(this._lowestId);
|
||||||
}
|
}
|
||||||
@@ -609,31 +519,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 +556,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++;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { imageType, IMetrics } from './IIPMetrics';
|
import { imageType } from './Metrics';
|
||||||
|
import type { IMetrics } from 'Types';
|
||||||
|
|
||||||
// fix missing nodejs decl
|
// fix missing nodejs decl
|
||||||
declare const require: (s: string) => any;
|
declare const require: (s: string) => any;
|
||||||
@@ -32,6 +33,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 }],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IMetrics } from './Types';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const UNSUPPORTED_TYPE: IMetrics = {
|
||||||
|
mime: 'unsupported',
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
export function imageType(d: Uint8Array): IMetrics {
|
||||||
|
if (d.length < 32) {
|
||||||
|
return UNSUPPORTED_TYPE;
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
// + first chunk must be IHDR
|
||||||
|
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
||||||
|
return {
|
||||||
|
mime: 'image/png',
|
||||||
|
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
||||||
|
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// JPEG: FF D8 FF
|
||||||
|
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
|
||||||
|
const [width, height] = jpgSize(d);
|
||||||
|
return { mime: 'image/jpeg', width, height };
|
||||||
|
}
|
||||||
|
// GIF: GIF87a or GIF89a
|
||||||
|
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
||||||
|
return {
|
||||||
|
mime: 'image/gif',
|
||||||
|
width: d[7] << 8 | d[6],
|
||||||
|
height: d[9] << 8 | d[8]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// QOI: qoif
|
||||||
|
if (d32[0] === 0x66696F71) {
|
||||||
|
return {
|
||||||
|
mime: 'image/qoi',
|
||||||
|
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function jpgSize(d: Uint8Array): [number, number] {
|
||||||
|
const len = d.length;
|
||||||
|
let i = 4;
|
||||||
|
let blockLength = d[i] << 8 | d[i + 1];
|
||||||
|
while (true) {
|
||||||
|
i += blockLength;
|
||||||
|
if (i >= len) {
|
||||||
|
// exhausted without size info
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
if (d[i] !== 0xFF) {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
|
||||||
|
if (i + 8 < len) {
|
||||||
|
return [
|
||||||
|
d[i + 7] << 8 | d[i + 8],
|
||||||
|
d[i + 5] << 8 | d[i + 6]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
i += 2;
|
||||||
|
blockLength = d[i] << 8 | d[i + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IDrawable } from './Types';
|
||||||
|
|
||||||
|
export function createCanvas(
|
||||||
|
localDocument: Document | undefined,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
cssWidth?: number,
|
||||||
|
cssHeight?: number
|
||||||
|
): HTMLCanvasElement {
|
||||||
|
/**
|
||||||
|
* NOTE: We normally dont care, from which document the canvas
|
||||||
|
* gets created, so we can fall back to global document,
|
||||||
|
* if the terminal has no document associated yet.
|
||||||
|
* This way early image loads before calling .open keep working
|
||||||
|
* (still discouraged though, as the metrics will be screwed up).
|
||||||
|
* Only the DOM output canvas should be on the terminal's document,
|
||||||
|
* which gets explicitly checked in `insertLayerToDom`.
|
||||||
|
*/
|
||||||
|
const canvas = (localDocument ?? document).createElement('canvas');
|
||||||
|
canvas.width = width | 0;
|
||||||
|
canvas.height = height | 0;
|
||||||
|
canvas.style.width = `${(cssWidth ?? width) | 0}px`;
|
||||||
|
canvas.style.height = `${(cssHeight ?? height) | 0}px`;
|
||||||
|
return canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drawable is a resource the ImageRenderer knows to draw on the screen.
|
||||||
|
*/
|
||||||
|
export class Drawable implements IDrawable {
|
||||||
|
public readonly width: number;
|
||||||
|
public readonly height: number;
|
||||||
|
// @readonly: only for memory tracking
|
||||||
|
public bytes: number;
|
||||||
|
|
||||||
|
constructor(public native: ImageBitmap | VideoFrame | HTMLCanvasElement) {
|
||||||
|
if (native instanceof VideoFrame) {
|
||||||
|
this.width = native.displayWidth;
|
||||||
|
this.height = native.displayHeight;
|
||||||
|
// NOTE: native.allocationSize() is too expensive
|
||||||
|
this.bytes = native.codedWidth * native.codedHeight * 4;
|
||||||
|
} else {
|
||||||
|
this.width = native.width;
|
||||||
|
this.height = native.height;
|
||||||
|
this.bytes = native.width * native.height * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public close(): void {
|
||||||
|
if (this.bytes === 0) return;
|
||||||
|
this.bytes = 0;
|
||||||
|
try {
|
||||||
|
if (this.native instanceof HTMLCanvasElement) {
|
||||||
|
this.native.width = 0;
|
||||||
|
this.native.height = 0;
|
||||||
|
} else {
|
||||||
|
this.native.close?.();
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
this.native = null!;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,9 +7,9 @@ import { SixelImageStorage } from './SixelImageStorage';
|
|||||||
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
||||||
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
||||||
import { RGBA8888 } from 'sixel/lib/Types';
|
import { RGBA8888 } from 'sixel/lib/Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
|
||||||
|
|
||||||
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
|
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
|
||||||
|
import { createCanvas, Drawable } from './Primitives';
|
||||||
|
|
||||||
// always free decoder ressources after decoding if it exceeds this limit
|
// always free decoder ressources after decoding if it exceeds this limit
|
||||||
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
||||||
@@ -98,12 +98,12 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const canvas = ImageRenderer.createCanvas(undefined, width, height);
|
const canvas = createCanvas(undefined, width, height);
|
||||||
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
|
||||||
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
||||||
this._dec.release();
|
this._dec.release();
|
||||||
}
|
}
|
||||||
this._storage.addImage(canvas);
|
this._storage.addImage(new Drawable(canvas), undefined, { width, height, mime: 'image/sixel' });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||||
import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
|
import { IImageAddonOptions, ITerminalExt, IAddImageOpts, IDrawable, IMetrics } from './Types';
|
||||||
import { ImageRenderer } from './ImageRenderer';
|
import { ImageRenderer } from './ImageRenderer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,9 +27,9 @@ export class SixelImageStorage {
|
|||||||
* Add a sixel image to storage.
|
* Add a sixel image to storage.
|
||||||
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
||||||
*/
|
*/
|
||||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics): void {
|
||||||
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
||||||
this._storage.addImage(img, this._addImageOpts);
|
this._storage.addImage(src, data, metrics, this._addImageOpts);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,10 +39,7 @@ export class SixelImageStorage {
|
|||||||
*/
|
*/
|
||||||
public advanceCursor(height: number): void {
|
public advanceCursor(height: number): void {
|
||||||
if (this._opts.sixelScrolling) {
|
if (this._opts.sixelScrolling) {
|
||||||
let cellSize = this._renderer.cellSize;
|
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
|
||||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
|
||||||
cellSize = CELL_SIZE_DEFAULT;
|
|
||||||
}
|
|
||||||
const rows = Math.ceil(height / cellSize.height);
|
const rows = Math.ceil(height / cellSize.height);
|
||||||
for (let i = 1; i < rows; ++i) {
|
for (let i = 1; i < rows; ++i) {
|
||||||
this._terminal._core._inputHandler.lineFeed();
|
this._terminal._core._inputHandler.lineFeed();
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ 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 'target-browser/Types';
|
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||||
import type { IRenderDimensions } from 'target-browser/renderer/shared/Types';
|
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import type { ICoreBrowserService, IRenderService, IThemeService } from 'target-browser/services/Services';
|
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||||
|
|
||||||
export const enum Cell {
|
export const enum Cell {
|
||||||
CONTENT = 0, // codepoint and wcwidth information (enum Content)
|
CONTENT = 0, // codepoint and wcwidth information (enum Content)
|
||||||
@@ -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.
|
||||||
@@ -107,16 +94,70 @@ export interface IAddImageOpts {
|
|||||||
layer: ImageLayer;
|
layer: ImageLayer;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
cursorPos: CursorPos;
|
cursorPos: CursorPos;
|
||||||
|
prescaleX?: number;
|
||||||
|
prescaleY?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDrawable {
|
||||||
|
native: ImageBitmap | VideoFrame | HTMLCanvasElement;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
close(): void;
|
||||||
|
readonly bytes: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ImageType = 'unsupported'
|
||||||
|
| 'image/png'
|
||||||
|
| 'image/jpeg'
|
||||||
|
| 'image/gif'
|
||||||
|
| 'image/qoi'
|
||||||
|
| 'image/webp'
|
||||||
|
| 'image/avif'
|
||||||
|
// sixel
|
||||||
|
| 'image/sixel'
|
||||||
|
// rgb|a blobs for kitty
|
||||||
|
| 'image/rgb'
|
||||||
|
| 'image/rgba';
|
||||||
|
|
||||||
|
export interface IMetrics {
|
||||||
|
mime: ImageType;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IImageSpec {
|
export interface IImageSpec {
|
||||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
/** entries for a later IImageSource type */
|
||||||
origCellSize: ICellSize;
|
/** drawable for screen rendering */
|
||||||
actual: HTMLCanvasElement | ImageBitmap | undefined;
|
src: IDrawable;
|
||||||
actualCellSize: ICellSize;
|
/** image bytes as blob */
|
||||||
|
data: Blob | undefined;
|
||||||
|
/** metrics about image like mime and dimensions */
|
||||||
|
metrics: IMetrics;
|
||||||
|
|
||||||
|
/** entries for a later IPlacement type */
|
||||||
|
/** cell size at time of insert */
|
||||||
|
cellSize: ICellSize;
|
||||||
|
/** eviction marker */
|
||||||
marker: IMarker | undefined;
|
marker: IMarker | undefined;
|
||||||
|
/** used tiles as eviction hint */
|
||||||
tileCount: number;
|
tileCount: number;
|
||||||
|
/** buffer it was placed on */
|
||||||
bufferType: 'alternate' | 'normal';
|
bufferType: 'alternate' | 'normal';
|
||||||
|
/** layer it was placed on */
|
||||||
layer: ImageLayer;
|
layer: ImageLayer;
|
||||||
zIndex: number;
|
zIndex: number;
|
||||||
|
/** scaling factor on source dimensions, default = 1.0 */
|
||||||
|
prescaleX: number;
|
||||||
|
prescaleY: number;
|
||||||
|
/** crop offset into source, default = 0 (currently unused) */
|
||||||
|
precropX: number;
|
||||||
|
precropY: number;
|
||||||
|
/** output scaling, default = 1.0 (currently unused) */
|
||||||
|
scaleX: number;
|
||||||
|
scaleY: number;
|
||||||
|
/** output offset, default = 0 (currently unused) */
|
||||||
|
offsetX: number;
|
||||||
|
offsetY: number;
|
||||||
|
/** compositing mode on output, default = 'alpha' (currently unused) */
|
||||||
|
blendMode: 'overwrite' | 'alpha';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
KittyPixelConstants,
|
KittyPixelConstants,
|
||||||
parseKittyCommand
|
parseKittyCommand
|
||||||
} from './KittyGraphicsTypes';
|
} from './KittyGraphicsTypes';
|
||||||
|
import { createCanvas } from '../Primitives';
|
||||||
|
|
||||||
const enum Constants {
|
const enum Constants {
|
||||||
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
// Memory limit for base64 decoder (4MB, same as IIPHandler)
|
||||||
@@ -573,8 +574,8 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
bitmap = cropped;
|
bitmap = cropped;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
const cw = this._renderer.dimensions?.device.cell.width || CELL_SIZE_DEFAULT.width;
|
||||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
const ch = this._renderer.dimensions?.device.cell.height || CELL_SIZE_DEFAULT.height;
|
||||||
|
|
||||||
// Per spec: c/r default to image's natural cell dimensions.
|
// Per spec: c/r default to image's natural cell dimensions.
|
||||||
// If only one of c/r is specified, compute the other from image aspect ratio.
|
// If only one of c/r is specified, compute the other from image aspect ratio.
|
||||||
@@ -636,7 +637,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
// the natural cell dimensions.
|
// the natural cell dimensions.
|
||||||
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
|
||||||
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
|
||||||
const offsetCanvas = ImageRenderer.createCanvas(window.document, canvasW, canvasH);
|
const offsetCanvas = createCanvas(window.document, canvasW, canvasH);
|
||||||
const offsetCtx = offsetCanvas.getContext('2d');
|
const offsetCtx = offsetCanvas.getContext('2d');
|
||||||
if (!offsetCtx) {
|
if (!offsetCtx) {
|
||||||
throw new Error('Failed to create offset canvas context');
|
throw new Error('Failed to create offset canvas context');
|
||||||
@@ -700,7 +701,7 @@ export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDispos
|
|||||||
return new Promise<ImageBitmap>((resolve, reject) => {
|
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||||
img.addEventListener('load', () => {
|
img.addEventListener('load', () => {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
const canvas = ImageRenderer.createCanvas(window.document, img.width, img.height);
|
const canvas = createCanvas(window.document, img.width, img.height);
|
||||||
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
canvas.getContext('2d')?.drawImage(img, 0, 0);
|
||||||
createImageBitmap(canvas).then(resolve).catch(reject);
|
createImageBitmap(canvas).then(resolve).catch(reject);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { IDisposable } from '@xterm/xterm';
|
|||||||
import { ImageStorage } from '../ImageStorage';
|
import { ImageStorage } from '../ImageStorage';
|
||||||
import { ImageLayer, IAddImageOpts } from '../Types';
|
import { ImageLayer, IAddImageOpts } from '../Types';
|
||||||
import { IKittyImageData } from './KittyGraphicsTypes';
|
import { IKittyImageData } from './KittyGraphicsTypes';
|
||||||
|
import { Drawable } from '../Primitives';
|
||||||
|
import { UNSUPPORTED_TYPE } from '../Metrics';
|
||||||
|
|
||||||
// Kitty-specific image storage controller.
|
// Kitty-specific image storage controller.
|
||||||
//
|
//
|
||||||
@@ -102,7 +104,8 @@ export class KittyImageStorage implements IDisposable {
|
|||||||
this._addImageOpts.scrolling = scrolling;
|
this._addImageOpts.scrolling = scrolling;
|
||||||
this._addImageOpts.layer = layer;
|
this._addImageOpts.layer = layer;
|
||||||
this._addImageOpts.zIndex = zIndex;
|
this._addImageOpts.zIndex = zIndex;
|
||||||
const storageId = this._storage.addImage(image, this._addImageOpts);
|
// FIXME: store image data in blob, fill metrics correctly
|
||||||
|
const storageId = this._storage.addImage(new Drawable(image), undefined, UNSUPPORTED_TYPE, this._addImageOpts);
|
||||||
this._kittyIdToStorageId.set(kittyId, storageId);
|
this._kittyIdToStorageId.set(kittyId, storageId);
|
||||||
this._storageIdToKittyId.set(storageId, kittyId);
|
this._storageIdToKittyId.set(storageId, kittyId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,8 @@
|
|||||||
"../../../node_modules/@types/mocha"
|
"../../../node_modules/@types/mocha"
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"target-browser/*": [ "../../../src/target-browser/*" ],
|
"browser/*": [ "../../../src/browser/*" ],
|
||||||
"common/*": [
|
"common/*": [ "../../../src/common/*" ],
|
||||||
"../../../src/primitives/*",
|
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
|
||||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
|
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
|
||||||
"*": [ "./*" ]
|
"*": [ "./*" ]
|
||||||
}
|
}
|
||||||
@@ -26,7 +23,7 @@
|
|||||||
"../../../typings/xterm.d.ts"
|
"../../../typings/xterm.d.ts"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src/target-browser" },
|
{ "path": "../../../src/browser" },
|
||||||
{ "path": "../../../src" }
|
{ "path": "../../../src/common" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,10 +294,10 @@ test.describe('ImageAddon', () => {
|
|||||||
test('size params is optional/informative', async () => {
|
test('size params is optional/informative', async () => {
|
||||||
// too small size param
|
// too small size param
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
// no size aparm at all
|
// no size aparm at all
|
||||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
deepStrictEqual(await getImageSize(2), [640, 80]);
|
||||||
});
|
});
|
||||||
test.skip('FilePart support - bytewise', async () => {
|
test.skip('FilePart support - bytewise', async () => {
|
||||||
// opener
|
// opener
|
||||||
@@ -308,7 +308,7 @@ test.describe('ImageAddon', () => {
|
|||||||
}
|
}
|
||||||
// finalizer
|
// finalizer
|
||||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
});
|
});
|
||||||
test('FilePart support - chunks', async () => {
|
test('FilePart support - chunks', async () => {
|
||||||
// opener
|
// opener
|
||||||
@@ -319,7 +319,7 @@ test.describe('ImageAddon', () => {
|
|||||||
}
|
}
|
||||||
// finalizer
|
// finalizer
|
||||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
});
|
});
|
||||||
test('FilePart support - edgecases', async () => {
|
test('FilePart support - edgecases', async () => {
|
||||||
// multiple opener do not harm
|
// multiple opener do not harm
|
||||||
@@ -347,39 +347,39 @@ test.describe('ImageAddon', () => {
|
|||||||
// finalizer skipped
|
// finalizer skipped
|
||||||
// write spinfox.png
|
// write spinfox.png
|
||||||
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('IIP support - testimages', () => {
|
test.describe('IIP support - testimages', () => {
|
||||||
test('palette.png', async () => {
|
test('palette.png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[0][0]);
|
await ctx.proxy.write(TESTDATA_IIP[0][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[0][1]);
|
||||||
});
|
});
|
||||||
test('spinfox.png', async () => {
|
test('spinfox.png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]);
|
||||||
});
|
});
|
||||||
test('w3c gif', async () => {
|
test('w3c gif', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[2][0]);
|
await ctx.proxy.write(TESTDATA_IIP[2][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[2][1]);
|
||||||
});
|
});
|
||||||
test('w3c jpeg', async () => {
|
test('w3c jpeg', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[3][0]);
|
await ctx.proxy.write(TESTDATA_IIP[3][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[3][1]);
|
||||||
});
|
});
|
||||||
test('w3c png', async () => {
|
test('w3c png', async () => {
|
||||||
await ctx.proxy.write(TESTDATA_IIP[4][0]);
|
await ctx.proxy.write(TESTDATA_IIP[4][0]);
|
||||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
|
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[4][1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('IIP - QOI support', () => {
|
test.describe('IIP - QOI support', () => {
|
||||||
test('palette should yield same bytes from PNG and QOI', async () => {
|
test('palette should yield same bytes from PNG and QOI', async () => {
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
deepStrictEqual(await getImageSize(1), [640, 80]);
|
||||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
|
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
deepStrictEqual(await getImageSize(2), [640, 80]);
|
||||||
const pngScrape = await getImageAtBufferCell(0, 0);
|
const pngScrape = await getImageAtBufferCell(0, 0);
|
||||||
const qoiScrape = await getImageAtBufferCell(0, 11);
|
const qoiScrape = await getImageAtBufferCell(0, 11);
|
||||||
deepStrictEqual(qoiScrape, pngScrape);
|
deepStrictEqual(qoiScrape, pngScrape);
|
||||||
@@ -406,20 +406,20 @@ test.describe('ImageAddon', () => {
|
|||||||
const header = 'width=20;height=5;preserveAspectRatio=0';
|
const header = 'width=20;height=5;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
deepStrictEqual(await getOrigSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
|
deepStrictEqual(await getImageSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
|
||||||
});
|
});
|
||||||
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
|
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
|
||||||
// pixel based resize
|
// pixel based resize
|
||||||
const header = 'width=320px;height=160px;preserveAspectRatio=0';
|
const header = 'width=320px;height=160px;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(1), [320, 160]);
|
deepStrictEqual(await getImageSize(1), [320, 160]);
|
||||||
});
|
});
|
||||||
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
|
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
|
||||||
// % of viewport resize
|
// % of viewport resize
|
||||||
const header = 'width=50%;height=30%;preserveAspectRatio=0';
|
const header = 'width=50%;height=30%;preserveAspectRatio=0';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
deepStrictEqual(await getOrigSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
|
deepStrictEqual(await getImageSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
|
||||||
});
|
});
|
||||||
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
|
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
|
||||||
// width provided in percent
|
// width provided in percent
|
||||||
@@ -427,14 +427,23 @@ test.describe('ImageAddon', () => {
|
|||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
|
||||||
const dim = await getDimensions();
|
const dim = await getDimensions();
|
||||||
const width = Math.floor(dim.width * 0.5);
|
const width = Math.floor(dim.width * 0.5);
|
||||||
deepStrictEqual(await getOrigSize(1), [width, Math.floor(width * 80 / 640)]);
|
deepStrictEqual(await getImageSize(1), [width, Math.floor(width * 80 / 640)]);
|
||||||
// height provided in pixel
|
// height provided in pixel
|
||||||
const header2 = 'height=200px';
|
const header2 = 'height=200px';
|
||||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
|
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
|
||||||
deepStrictEqual(await getOrigSize(2), [Math.floor(200 * 640 / 80), 200]);
|
deepStrictEqual(await getImageSize(2), [Math.floor(200 * 640 / 80), 200]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('text overwrite removes tiles', () => {
|
||||||
|
test('sixel', async () => {
|
||||||
|
await assertTextClearsTiles(SIXEL_SEQ_0);
|
||||||
|
});
|
||||||
|
test('iip', async () => {
|
||||||
|
await assertTextClearsTiles(TESTDATA_IIP[0][0]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -443,10 +452,10 @@ test.describe('ImageAddon', () => {
|
|||||||
async function getDimensions(): Promise<IDimensions> {
|
async function getDimensions(): Promise<IDimensions> {
|
||||||
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
||||||
return {
|
return {
|
||||||
cellWidth: Math.round(dimensions.css.cell.width),
|
cellWidth: Math.round(dimensions.device.cell.width),
|
||||||
cellHeight: Math.round(dimensions.css.cell.height),
|
cellHeight: Math.round(dimensions.device.cell.height),
|
||||||
width: Math.round(dimensions.css.canvas.width),
|
width: Math.round(dimensions.device.canvas.width),
|
||||||
height: Math.round(dimensions.css.canvas.height)
|
height: Math.round(dimensions.device.canvas.height)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -462,13 +471,30 @@ async function getScrollbackPlusRows(): Promise<number> {
|
|||||||
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
|
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
async function getImageSize(id: number): Promise<[number, number]> {
|
||||||
return ctx.page.evaluate<any>(`[
|
return ctx.page.evaluate<any>(`[
|
||||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
window.imageAddon._storage._images.get(${id}).src.width * window.imageAddon._storage._images.get(${id}).prescaleX,
|
||||||
window.imageAddon._storage._images.get(${id}).orig.height
|
window.imageAddon._storage._images.get(${id}).src.height * window.imageAddon._storage._images.get(${id}).prescaleY
|
||||||
]`);
|
]`).then(r => r.map(Math.floor));
|
||||||
}
|
}
|
||||||
|
|
||||||
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.getImageAtBufferCell(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
|
||||||
|
await ctx.proxy.write('\x1b[H' + imageSeq);
|
||||||
|
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(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('#######'));
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(seq);
|
await ctx.proxy.write(seq);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [1, 1]);
|
deepStrictEqual(await getImageSize(1), [1, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores 3x1 RGB PNG with a=T', async () => {
|
test('stores 3x1 RGB PNG with a=T', async () => {
|
||||||
@@ -131,7 +131,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(seq);
|
await ctx.proxy.write(seq);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transmit only (a=t) does not display but stores in handler', async () => {
|
test('transmit only (a=t) does not display but stores in handler', async () => {
|
||||||
@@ -1107,7 +1107,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [20, 50]);
|
deepStrictEqual(await getImageSize(1), [20, 50]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1283,7 +1283,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
|
|
||||||
const cursor = await getCursor();
|
const cursor = await getCursor();
|
||||||
const cellDims: number[] = await ctx.page.evaluate(() => {
|
const cellDims: number[] = await ctx.page.evaluate(() => {
|
||||||
const d = (window as any).term._core._renderService.dimensions.css.cell;
|
const d = (window as any).term._core._renderService.dimensions.device.cell;
|
||||||
return [d.width, d.height];
|
return [d.width, d.height];
|
||||||
});
|
});
|
||||||
const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]);
|
const expectedR = Math.ceil((100 / 200) * 10 * cellDims[0] / cellDims[1]);
|
||||||
@@ -1301,7 +1301,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
|
|
||||||
const cursor = await getCursor();
|
const cursor = await getCursor();
|
||||||
const cellDims: number[] = await ctx.page.evaluate(() => {
|
const cellDims: number[] = await ctx.page.evaluate(() => {
|
||||||
const d = (window as any).term._core._renderService.dimensions.css.cell;
|
const d = (window as any).term._core._renderService.dimensions.device.cell;
|
||||||
return [d.width, d.height];
|
return [d.width, d.height];
|
||||||
});
|
});
|
||||||
const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]);
|
const expectedC = Math.ceil((200 / 100) * 5 * cellDims[1] / cellDims[0]);
|
||||||
@@ -1535,7 +1535,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('transmit only (a=t) stores 200x100 image without display', async () => {
|
test('transmit only (a=t) stores 200x100 image without display', async () => {
|
||||||
@@ -1564,7 +1564,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=500;${part2}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles 3-chunk transmission', async () => {
|
test('handles 3-chunk transmission', async () => {
|
||||||
@@ -1580,7 +1580,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,i=501;${p3}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('verifies chunked data assembles correctly', async () => {
|
test('verifies chunked data assembles correctly', async () => {
|
||||||
@@ -1713,7 +1713,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=20,y=0,w=20,h=50;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [20, 50]);
|
deepStrictEqual(await getImageSize(1), [20, 50]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 128, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 19, 49), [255, 128, 0, 255]);
|
||||||
});
|
});
|
||||||
@@ -1747,7 +1747,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,w=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
|
deepStrictEqual(await getPixel(0, 0, 199, 99), [255, 255, 255, 255]);
|
||||||
});
|
});
|
||||||
@@ -1756,7 +1756,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,h=0;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 50), [255, 192, 203, 255]);
|
||||||
});
|
});
|
||||||
@@ -1772,7 +1772,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=100,x=-10,y=-10;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||||
await timeout(200);
|
await timeout(200);
|
||||||
|
|
||||||
deepStrictEqual(await getOrigSize(1), [200, 100]);
|
deepStrictEqual(await getImageSize(1), [200, 100]);
|
||||||
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
deepStrictEqual(await getPixel(0, 0, 0, 0), [255, 0, 0, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1893,28 +1893,28 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=3,v=1;${RAW_RGB_3X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (2x2)', async () => {
|
test('stores image with correct original dimensions (2x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=2,v=2;${RAW_RGB_2X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [2, 2]);
|
deepStrictEqual(await getImageSize(1), [2, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (5x1)', async () => {
|
test('stores image with correct original dimensions (5x1)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=5,v=1;${RAW_RGB_5X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [5, 1]);
|
deepStrictEqual(await getImageSize(1), [5, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (4x2)', async () => {
|
test('stores image with correct original dimensions (4x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=24,s=4,v=2;${RAW_RGB_4X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [4, 2]);
|
deepStrictEqual(await getImageSize(1), [4, 2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2034,21 +2034,21 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=3,v=1;${RAW_RGBA_3X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [3, 1]);
|
deepStrictEqual(await getImageSize(1), [3, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (2x2)', async () => {
|
test('stores image with correct original dimensions (2x2)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=2,v=2;${RAW_RGBA_2X2}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [2, 2]);
|
deepStrictEqual(await getImageSize(1), [2, 2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stores image with correct original dimensions (5x1)', async () => {
|
test('stores image with correct original dimensions (5x1)', async () => {
|
||||||
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
|
await ctx.proxy.write(`\x1b_Ga=T,f=32,s=5,v=1;${RAW_RGBA_5X1}\x1b\\`);
|
||||||
await timeout(100);
|
await timeout(100);
|
||||||
strictEqual(await getImageStorageLength(), 1);
|
strictEqual(await getImageStorageLength(), 1);
|
||||||
deepStrictEqual(await getOrigSize(1), [5, 1]);
|
deepStrictEqual(await getImageSize(1), [5, 1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -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.getImageAtBufferCell(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('#######'));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -2221,10 +2236,10 @@ test.describe('Kitty Graphics Protocol', () => {
|
|||||||
async function getDimensions(): Promise<IDimensions> {
|
async function getDimensions(): Promise<IDimensions> {
|
||||||
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
|
||||||
return {
|
return {
|
||||||
cellWidth: Math.round(dimensions.css.cell.width),
|
cellWidth: Math.round(dimensions.device.cell.width),
|
||||||
cellHeight: Math.round(dimensions.css.cell.height),
|
cellHeight: Math.round(dimensions.device.cell.height),
|
||||||
width: Math.round(dimensions.css.canvas.width),
|
width: Math.round(dimensions.device.canvas.width),
|
||||||
height: Math.round(dimensions.css.canvas.height)
|
height: Math.round(dimensions.device.canvas.height)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2236,10 +2251,10 @@ async function getImageStorageLength(): Promise<number> {
|
|||||||
return ctx.page.evaluate('window.imageAddon._storage._images.size');
|
return ctx.page.evaluate('window.imageAddon._storage._images.size');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
async function getImageSize(id: number): Promise<[number, number]> {
|
||||||
return ctx.page.evaluate<any>(`[
|
return ctx.page.evaluate<any>(`[
|
||||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
window.imageAddon._storage._images.get(${id}).src.width,
|
||||||
window.imageAddon._storage._images.get(${id}).orig.height
|
window.imageAddon._storage._images.get(${id}).src.height
|
||||||
]`);
|
]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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.getImageAtBufferCell(${x}, ${y})`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -125,10 +125,5 @@ declare module '@xterm/addon-image' {
|
|||||||
* Get original image canvas at buffer position.
|
* Get original image canvas at buffer position.
|
||||||
*/
|
*/
|
||||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract single tile canvas at buffer position.
|
|
||||||
*/
|
|
||||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ const addon = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
browser: path.resolve('../../out/browser'),
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
browser: path.resolve('../../out/target-browser'),
|
|
||||||
vs: path.resolve('../../out/vs')
|
vs: path.resolve('../../out/vs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -16,11 +16,10 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"@xterm/addon-progress": [
|
"@xterm/addon-progress": [
|
||||||
"../typings/addon-progress.d.ts"
|
"../typings/addon-progress.d.ts"
|
||||||
@@ -33,10 +32,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ module.exports = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
browser: path.resolve('../../out/browser'),
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
browser: path.resolve('../../out/target-browser'),
|
|
||||||
vs: path.resolve('../../out/vs')
|
vs: path.resolve('../../out/vs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SearchEngine } from './SearchEngine';
|
import { SearchEngine } from './SearchEngine';
|
||||||
import { SearchLineCache } from './SearchLineCache';
|
import { SearchLineCache } from './SearchLineCache';
|
||||||
import { Terminal } from 'target-browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import type { ISearchOptions } from '@xterm/addon-search';
|
import type { ISearchOptions } from '@xterm/addon-search';
|
||||||
import { DisposableStore } from 'common/Lifecycle';
|
import { DisposableStore } from 'common/Lifecycle';
|
||||||
|
|
||||||
|
|||||||
@@ -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`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
||||||
import { Terminal } from 'target-browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import { timeout } from 'common/Async';
|
import { timeout } from 'common/Async';
|
||||||
|
|
||||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||||
|
|||||||
@@ -16,11 +16,10 @@
|
|||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"@xterm/addon-search" : [
|
"@xterm/addon-search" : [
|
||||||
"../typings/addon-search.d.ts"
|
"../typings/addon-search.d.ts"
|
||||||
@@ -33,10 +32,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ module.exports = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
vs: path.resolve('../../out/vs')
|
vs: path.resolve('../../out/vs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
|||||||
|
|
||||||
import { spawn } from 'node-pty';
|
import { spawn } from 'node-pty';
|
||||||
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||||
import { Terminal } from 'target-browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import { SerializeAddon } from 'SerializeAddon';
|
import { SerializeAddon } from 'SerializeAddon';
|
||||||
|
|
||||||
class TestTerminal extends Terminal {
|
class TestTerminal extends Terminal {
|
||||||
|
|||||||
@@ -12,11 +12,8 @@
|
|||||||
"target": "es2021",
|
"target": "es2021",
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": ["../../../src/common/*"],
|
||||||
"../../../src/primitives/*",
|
"browser/*": ["../../../src/browser/*"],
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
|
||||||
"target-browser/*": ["../../../src/target-browser/*"],
|
|
||||||
"SerializeAddon": ["../src/SerializeAddon"],
|
"SerializeAddon": ["../src/SerializeAddon"],
|
||||||
"@xterm/addon-serialize": [
|
"@xterm/addon-serialize": [
|
||||||
"../typings/addon-serialize.d.ts"
|
"../typings/addon-serialize.d.ts"
|
||||||
@@ -26,7 +23,7 @@
|
|||||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src" },
|
{ "path": "../../../src/common" },
|
||||||
{ "path": "../../../src/target-browser" }
|
{ "path": "../../../src/browser" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package",
|
"prepublishOnly": "npm run package",
|
||||||
"start": "node ../../demo/start",
|
"start": "node ../../demo/start",
|
||||||
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
|
"benchmark": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
|
||||||
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
"benchmark-baseline": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
||||||
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
"benchmark-eval": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
import jsdom = require('jsdom');
|
import jsdom = require('jsdom');
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { SerializeAddon } from './SerializeAddon';
|
import { SerializeAddon } from './SerializeAddon';
|
||||||
import { Terminal } from 'target-browser/public/Terminal';
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
import { SelectionModel } from 'target-browser/selection/SelectionModel';
|
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||||
import { IBufferService } from 'common/services/Services';
|
import { IBufferService } from 'common/services/Services';
|
||||||
import { ThemeService } from 'target-browser/services/ThemeService';
|
import { ThemeService } from 'browser/services/ThemeService';
|
||||||
|
|
||||||
function sgr(...seq: string[]): string {
|
function sgr(...seq: string[]): string {
|
||||||
return `\x1b[${seq.join(';')}m`;
|
return `\x1b[${seq.join(';')}m`;
|
||||||
|
|||||||
@@ -7,8 +7,9 @@
|
|||||||
|
|
||||||
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 { DEFAULT_ANSI_COLORS } from 'target-browser/Types';
|
import { IAttributeData } from 'common/buffer/Types';
|
||||||
|
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||||
|
|
||||||
function constrain(value: number, low: number, high: number): number {
|
function constrain(value: number, low: number, high: number): number {
|
||||||
@@ -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)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -12,11 +12,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"@xterm/addon-serialize": [
|
"@xterm/addon-serialize": [
|
||||||
"../typings/addon-serialize.d.ts"
|
"../typings/addon-serialize.d.ts"
|
||||||
@@ -34,10 +33,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -29,10 +28,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -25,11 +25,8 @@ module.exports = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
browser: path.resolve('../../out/browser')
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
browser: path.resolve('../../out/target-browser')
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
output: {
|
output: {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
|||||||
|
|
||||||
import { spawn } from 'node-pty';
|
import { spawn } from 'node-pty';
|
||||||
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||||
import { CoreBrowserTerminal } from 'target-browser/CoreBrowserTerminal';
|
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
|
||||||
import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider';
|
import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,8 @@
|
|||||||
"target": "es2015",
|
"target": "es2015",
|
||||||
"module": "nodenext",
|
"module": "nodenext",
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": ["../../../src/common/*"],
|
||||||
"../../../src/primitives/*",
|
"browser/*": ["../../../src/browser/*"],
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
|
||||||
"target-browser/*": ["../../../src/target-browser/*"],
|
|
||||||
"UnicodeGraphemeProvider": ["../src/UnicodeGraphemeProvider"],
|
"UnicodeGraphemeProvider": ["../src/UnicodeGraphemeProvider"],
|
||||||
"@xterm/addon-unicode-graphemes": [
|
"@xterm/addon-unicode-graphemes": [
|
||||||
"../typings/addon-unicode-graphemes.d.ts"
|
"../typings/addon-unicode-graphemes.d.ts"
|
||||||
@@ -23,7 +20,7 @@
|
|||||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src" },
|
{ "path": "../../../src/common" },
|
||||||
{ "path": "../../../src/target-browser" }
|
{ "path": "../../../src/browser" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package",
|
"prepublishOnly": "npm run package",
|
||||||
"start": "node ../../demo/start",
|
"start": "node ../../demo/start",
|
||||||
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
|
"benchmark": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
|
||||||
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
"benchmark-baseline": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
||||||
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
"benchmark-eval": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IUnicodeVersionProvider } from '@xterm/xterm';
|
import { IUnicodeVersionProvider } from '@xterm/xterm';
|
||||||
import { createPropertyValue, extractCharKind, extractWidth } from 'common/input/UnicodeProperties';
|
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services';
|
||||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/input/UnicodeTypes';
|
import { UnicodeService } from 'common/services/UnicodeService';
|
||||||
import * as UC from './third-party/UnicodeProperties';
|
import * as UC from './third-party/UnicodeProperties';
|
||||||
|
|
||||||
export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
||||||
@@ -19,7 +19,7 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static readonly _plainNarrowProperties: UnicodeCharProperties
|
private static readonly _plainNarrowProperties: UnicodeCharProperties
|
||||||
= createPropertyValue(UC.GRAPHEME_BREAK_Other, 1, false);
|
= UnicodeService.createPropertyValue(UC.GRAPHEME_BREAK_Other, 1, false);
|
||||||
|
|
||||||
public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {
|
public charProperties(codepoint: number, preceding: UnicodeCharProperties): UnicodeCharProperties {
|
||||||
// Optimize the simple ASCII case, under the condition that
|
// Optimize the simple ASCII case, under the condition that
|
||||||
@@ -39,9 +39,9 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
|||||||
w = 1;
|
w = 1;
|
||||||
}
|
}
|
||||||
if (preceding !== 0) {
|
if (preceding !== 0) {
|
||||||
const oldWidth = extractWidth(preceding);
|
const oldWidth = UnicodeService.extractWidth(preceding);
|
||||||
if (this.handleGraphemes) {
|
if (this.handleGraphemes) {
|
||||||
charInfo = UC.shouldJoin(extractCharKind(preceding), charInfo);
|
charInfo = UC.shouldJoin(UnicodeService.extractCharKind(preceding), charInfo);
|
||||||
} else {
|
} else {
|
||||||
charInfo = w === 0 ? 1 : 0;
|
charInfo = w === 0 ? 1 : 0;
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,7 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createPropertyValue(charInfo, w, shouldJoin);
|
return UnicodeService.createPropertyValue(charInfo, w, shouldJoin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public wcwidth(codepoint: number): UnicodeCharWidth {
|
public wcwidth(codepoint: number): UnicodeCharWidth {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"@xterm/addon-unicode-graphemes": [
|
"@xterm/addon-unicode-graphemes": [
|
||||||
"../typings/addon-unicode-graphemes.d.ts"
|
"../typings/addon-unicode-graphemes.d.ts"
|
||||||
@@ -30,7 +29,7 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -29,10 +28,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ module.exports = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
vs: path.resolve('../../out/vs')
|
vs: path.resolve('../../out/vs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IUnicodeVersionProvider } from '@xterm/xterm';
|
import { IUnicodeVersionProvider } from '@xterm/xterm';
|
||||||
import { createPropertyValue, extractWidth } from 'common/input/UnicodeProperties';
|
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services';
|
||||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/input/UnicodeTypes';
|
import { UnicodeService } from 'common/services/UnicodeService';
|
||||||
|
|
||||||
const BMP_COMBINING = [
|
const BMP_COMBINING = [
|
||||||
[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD],
|
[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD],
|
||||||
@@ -223,13 +223,13 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
|
|||||||
let width = this.wcwidth(codepoint);
|
let width = this.wcwidth(codepoint);
|
||||||
let shouldJoin = width === 0 && preceding !== 0;
|
let shouldJoin = width === 0 && preceding !== 0;
|
||||||
if (shouldJoin) {
|
if (shouldJoin) {
|
||||||
const oldWidth = extractWidth(preceding);
|
const oldWidth = UnicodeService.extractWidth(preceding);
|
||||||
if (oldWidth === 0) {
|
if (oldWidth === 0) {
|
||||||
shouldJoin = false;
|
shouldJoin = false;
|
||||||
} else if (oldWidth > width) {
|
} else if (oldWidth > width) {
|
||||||
width = oldWidth;
|
width = oldWidth;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createPropertyValue(0, width, shouldJoin);
|
return UnicodeService.createPropertyValue(0, width, shouldJoin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,7 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"@xterm/addon-unicode11": [
|
"@xterm/addon-unicode11": [
|
||||||
"../typings/addon-unicode11.d.ts"
|
"../typings/addon-unicode11.d.ts"
|
||||||
@@ -33,7 +32,7 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -25,10 +25,7 @@ module.exports = {
|
|||||||
modules: ['./node_modules'],
|
modules: ['./node_modules'],
|
||||||
extensions: [ '.js' ],
|
extensions: [ '.js' ],
|
||||||
alias: {
|
alias: {
|
||||||
common: [
|
common: path.resolve('../../out/common'),
|
||||||
path.resolve('../../out/runtime'),
|
|
||||||
path.resolve('../../out/primitives'),
|
|
||||||
],
|
|
||||||
vs: path.resolve('../../out/vs')
|
vs: path.resolve('../../out/vs')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -11,11 +11,10 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [
|
"common/*": [
|
||||||
"../../../src/primitives/*",
|
"../../../src/common/*"
|
||||||
"../../../src/runtime/*",
|
|
||||||
],
|
],
|
||||||
"target-browser/*": [
|
"browser/*": [
|
||||||
"../../../src/target-browser/*"
|
"../../../src/browser/*"
|
||||||
],
|
],
|
||||||
"*": [
|
"*": [
|
||||||
"./*"
|
"./*"
|
||||||
@@ -32,10 +31,10 @@
|
|||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "../../../src"
|
"path": "../../../src/common"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../src/target-browser"
|
"path": "../../../src/browser"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "../../../test/playwright"
|
"path": "../../../test/playwright"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||