Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
22e01b7fdc | ||
|
|
2cc9ff0317 | ||
|
|
c58ea3637f | ||
|
|
3eaea160f4 | ||
|
|
c9b14056c5 | ||
|
|
c1d2d6853f | ||
|
|
42894ff9a1 | ||
|
|
8c8c228ac5 | ||
|
|
e54045d89b | ||
|
|
d6d95171a5 | ||
|
|
4d0a9c3419 | ||
|
|
de620dc826 | ||
|
|
651a3537dd | ||
|
|
f1ae0ab30b | ||
|
|
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:
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
@@ -69,7 +74,7 @@ jobs:
|
||||
./addons/addon-webgl/out/* \
|
||||
./addons/addon-webgl/out-*st/*
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: compressed-build.zip
|
||||
@@ -79,9 +84,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
@@ -100,16 +107,18 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -129,6 +138,7 @@ jobs:
|
||||
exit $EXIT_CODE
|
||||
|
||||
test-unit:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -136,21 +146,18 @@ jobs:
|
||||
runs-on: [ubuntu, macos, windows]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- name: Wait for build job
|
||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job: build
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -166,6 +173,7 @@ jobs:
|
||||
run: npm run test-unit --forbid-only
|
||||
|
||||
test-integration:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -174,9 +182,11 @@ jobs:
|
||||
browser: [chromium, firefox, webkit]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
@@ -185,12 +195,7 @@ jobs:
|
||||
npm ci
|
||||
- name: Install playwright
|
||||
run: npx playwright install --with-deps ${{ matrix.browser }}
|
||||
- name: Wait for build job
|
||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job: build
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -240,9 +245,11 @@ jobs:
|
||||
matrix:
|
||||
node-version: [22]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
@@ -251,7 +258,7 @@ jobs:
|
||||
npm ci
|
||||
- name: Install playwright
|
||||
run: npx playwright install
|
||||
- uses: actions/download-artifact@v4
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
|
||||
@@ -8,6 +8,11 @@ on:
|
||||
schedule:
|
||||
- cron: '41 17 * * 0'
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
@@ -24,12 +29,14 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
|
||||
|
||||
@@ -9,6 +9,9 @@ on:
|
||||
paths:
|
||||
- .github/workflows/copilot-setup-steps.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
copilot-setup-steps:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -18,9 +21,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
|
||||
@@ -6,13 +6,18 @@ on:
|
||||
# without running tests to save time to publish
|
||||
branches: [ "master" ]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
|
||||
@@ -17,6 +17,7 @@ docs/
|
||||
npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
mise.toml
|
||||
build/
|
||||
.DS_Store
|
||||
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",
|
||||
"${workspaceRoot}/out/**/*.test.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_PATH": "${workspaceRoot}/out"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
|
||||
@@ -2,13 +2,10 @@
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
**Core Structure**: xterm.js is a multi-target terminal emulator with these main source areas:
|
||||
- `src/primitives/`: Building blocks (buffer, parser, input, data, utilities); TypeScript project `src/primitives/tsconfig.json`
|
||||
- `src/runtime/`: Services, `CoreTerminal`, `InputHandler`, public API adapters; TypeScript project `src/runtime/tsconfig.json`
|
||||
- `src/target-browser/`: Browser terminal with DOM rendering
|
||||
- `src/target-headless/`: Node terminal without DOM
|
||||
|
||||
See `src/ARCHITECTURE.md` for the dependency graph and import-path conventions (`common/*`, `target-browser/*`, `target-headless/*`).
|
||||
**Core Structure**: xterm.js is a multi-target terminal emulator with three main distributions:
|
||||
- `src/browser/`: Full-featured browser terminal with DOM rendering
|
||||
- `src/headless/`: Server-side terminal for Node.js (no DOM)
|
||||
- `src/common/`: Shared core logic (parsing, buffer management, terminal state)
|
||||
|
||||
**Key Classes**:
|
||||
- `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 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>`
|
||||
- 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
|
||||
|
||||
## 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:
|
||||
|
||||
- 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_.
|
||||
- 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.
|
||||
|
||||
@@ -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.
|
||||
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
||||
- [**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)
|
||||
|
||||
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.describe('Search Tests', () => {
|
||||
test.describe('AttachAddon', () => {
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ctx.proxy.reset();
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"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 {
|
||||
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 {
|
||||
if ((Uint8Array as IUint8ArrayB64Ctor).fromBase64 !== undefined) {
|
||||
try {
|
||||
return new TextDecoder().decode((Uint8Array as IUint8ArrayB64Ctor).fromBase64(data));
|
||||
} catch {}
|
||||
return '';
|
||||
}
|
||||
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 {}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-clipboard": [
|
||||
"../typings/addon-clipboard.d.ts"
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"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.evaluate(`
|
||||
window.term.reset()
|
||||
window.term.reset();
|
||||
window.clipboard?.dispose();
|
||||
window.clipboard = new ClipboardAddon();
|
||||
window.term.loadAddon(window.clipboard);
|
||||
@@ -92,4 +92,22 @@ test.describe('ClipboardAddon', () => {
|
||||
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,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"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.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
export class BrowserClipboardProvider implements IClipboardProvider{
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
|
||||
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
|
||||
import { ViewportConstants } from 'target-browser/shared/Constants';
|
||||
import { ViewportConstants } from 'browser/shared/Constants';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
/**
|
||||
@@ -74,14 +74,14 @@ export class FitAddon implements ITerminalAddon, IFitApi {
|
||||
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
|
||||
|
||||
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const parentElementHeight = Math.max(0, parseInt(parentElementStyle.getPropertyValue('height'), 10) || 0);
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10) || 0);
|
||||
const elementStyle = _getComputedStyle(this._terminal.element);
|
||||
const elementPadding = {
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top'), 10) || 0,
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom'), 10) || 0,
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right'), 10) || 0,
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'), 10) || 0
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-fit": [
|
||||
"../typings/addon-fit.d.ts"
|
||||
@@ -29,7 +29,7 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -90,7 +90,7 @@ terminal.loadAddon(imageAddon);
|
||||
therefore palette animations cannot be used.
|
||||
|
||||
- **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.
|
||||
|
||||
@@ -98,7 +98,7 @@ terminal.loadAddon(imageAddon);
|
||||
Set by default, change it with `{iipSupport: true}`.
|
||||
|
||||
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.
|
||||
- Image type hinting is not supported (always deducted from data header).
|
||||
- File download is not supported.
|
||||
@@ -112,6 +112,13 @@ terminal.loadAddon(imageAddon);
|
||||
- ReportCellSize sequence is supported.
|
||||
- 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)**
|
||||
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
|
||||
|
||||
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:
|
||||
- Incomming 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.
|
||||
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:
|
||||
- Incoming image data chunk at `term.write` (terminal)
|
||||
`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)
|
||||
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
|
||||
- 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.
|
||||
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?_
|
||||
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?_
|
||||
- `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`
|
||||
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`
|
||||
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
|
||||
|
||||
|
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 |
@@ -0,0 +1,630 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import type { IDisposable } from '@xterm/xterm';
|
||||
import type { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageTileInfo, type ImageStorage } from './ImageStorage';
|
||||
import type { IImageSpec, ITerminalExt, IResetHandler } from './Types';
|
||||
import type { IMetrics } from 'IIPMetrics';
|
||||
|
||||
|
||||
/**
|
||||
* Note on Firefox Bug (https://bugzilla.mozilla.org/show_bug.cgi?id=2066911)
|
||||
*
|
||||
* ImageDecoder.decode() returns a corrupted VideoFrame,
|
||||
* if the frame was closed from a previous decode.
|
||||
*
|
||||
* Therefore the AnimationManager class implements certain
|
||||
* firefox shims gated by the isFirefox boolean:
|
||||
* - IAnimation.highest tracks highest frameID ever decoded
|
||||
* - whenever frameID is lesser or equal zu highest create new decoder
|
||||
* - raw image bytes need to be held for new decoder
|
||||
*
|
||||
* To be removed once the bug got fixed.
|
||||
*/
|
||||
|
||||
|
||||
interface IAnimation {
|
||||
/** image id in storage */
|
||||
id: number;
|
||||
/** image bytes */
|
||||
data: Blob;
|
||||
/** metrics of the image */
|
||||
metrics: IMetrics;
|
||||
/** width */
|
||||
w: number;
|
||||
/** height */
|
||||
h: number;
|
||||
/** number of frames */
|
||||
numFrames: number;
|
||||
frames: number[];
|
||||
/** ImageDecoder instance */
|
||||
decoder: ImageDecoder | undefined;
|
||||
decoderPromise: Promise<ImageDecoder> | undefined;
|
||||
cache: BitmapBuffer;
|
||||
/** start timestamp */
|
||||
startTime: number | undefined;
|
||||
/** whether a decode task ist scheduled */
|
||||
scheduled: boolean;
|
||||
/** whether the image is in viewport */
|
||||
inViewport: boolean;
|
||||
/** firefox bug helper */
|
||||
highest: number;
|
||||
/** playtime of animation */
|
||||
playtime: number;
|
||||
/** frame timestamps */
|
||||
timestamps: number[];
|
||||
/** frame ID targeted by animation */
|
||||
current: number;
|
||||
/** frame ID currently visible (owned by spec.actual) */
|
||||
actual: number;
|
||||
/** idx of current on frames */
|
||||
frameIdx: number;
|
||||
}
|
||||
|
||||
|
||||
interface IImageDraw {
|
||||
imageId: number;
|
||||
imgSpec: IImageSpec;
|
||||
tileId: number;
|
||||
col: number;
|
||||
row: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
|
||||
interface IBufferSlot {
|
||||
id: number;
|
||||
bm: ImageBitmap | null;
|
||||
}
|
||||
|
||||
|
||||
const enum Constants {
|
||||
BUFFERSIZE = 4,
|
||||
BUFFERMOD = BUFFERSIZE - 1,
|
||||
MAX_FRAMES = 65535
|
||||
}
|
||||
|
||||
|
||||
class BitmapBuffer implements IDisposable {
|
||||
private readonly _d: IBufferSlot[];
|
||||
private _disposed = false;
|
||||
|
||||
constructor() {
|
||||
this._d = Array(Constants.BUFFERSIZE).fill(0).map(() => ({ id: -1, bm: null }));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clear();
|
||||
this._disposed = true;
|
||||
}
|
||||
|
||||
public peek(id: number): ImageBitmap | null {
|
||||
const slot = this._d[id & Constants.BUFFERMOD];
|
||||
return id === slot.id ? slot.bm : null;
|
||||
}
|
||||
|
||||
public pop(id: number): ImageBitmap | null {
|
||||
const slot = this._d[id & Constants.BUFFERMOD];
|
||||
const res = id === slot.id
|
||||
? slot.bm
|
||||
: (slot.bm?.close(), null);
|
||||
slot.bm = null;
|
||||
slot.id = -1;
|
||||
return res;
|
||||
}
|
||||
|
||||
public push(id: number, bm: ImageBitmap): void {
|
||||
/**
|
||||
* A late decode task might still push bitmaps.
|
||||
* Since push gives the buffer ownership of
|
||||
* the bitmap, we use the isDisposed flag to
|
||||
* free it right away.
|
||||
*/
|
||||
if (this._disposed) {
|
||||
bm.close();
|
||||
return;
|
||||
}
|
||||
const slot = this._d[id & Constants.BUFFERMOD];
|
||||
slot.bm?.close();
|
||||
slot.bm = bm;
|
||||
slot.id = id;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (let i = 0; i < Constants.BUFFERSIZE; ++i) {
|
||||
const slot = this._d[i];
|
||||
slot.bm?.close();
|
||||
slot.bm = null;
|
||||
slot.id = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const InstallTrigger: any;
|
||||
|
||||
|
||||
// FIXME: move to AnimationManager ctor to control it from xterm side
|
||||
const isChromium = (window as any).chrome !== undefined;
|
||||
const isFirefox: boolean = typeof InstallTrigger !== 'undefined' || 'MozAppearance' in document.documentElement.style;
|
||||
|
||||
|
||||
/**
|
||||
* Helper to create a read to use ImageDecoder instance from a blob.
|
||||
*/
|
||||
async function BlobDecoder(blob: Blob, type: string): Promise<ImageDecoder> {
|
||||
/**
|
||||
* Dealing with browser bugs - in theory this should be possible:
|
||||
*
|
||||
* const decoder = new ImageDecoder({ data: blob.stream(), type, preferAnimation: true });
|
||||
* await decoder.tracks.ready;
|
||||
* return decoder;
|
||||
*
|
||||
* But this straightforward handling with blob.stream() uncovers several bugs:
|
||||
* 1. Chromium: Fails to stream chunked blobs reliably.
|
||||
* 2. Firefox: Native lifecycle race condition. Closing an old decoder breaks the
|
||||
* shared underlying native stream of the same blob for new decoders, throwing
|
||||
* an uncatchable "Closed decoder" DOMException during `tracks.ready`.
|
||||
*
|
||||
* So we use a safer approach by pulling full raw bytes over first and transferring ownership.
|
||||
*/
|
||||
const data = await blob.arrayBuffer();
|
||||
const decoder = new ImageDecoder({ data, type, preferAnimation: true, transfer: [data] });
|
||||
await decoder.tracks.ready;
|
||||
if (isChromium && type === 'image/avif') {
|
||||
// chromium bug: random frame access in AVIF is
|
||||
// only possible after reading frame 0
|
||||
const res = await decoder.decode({ frameIndex: 0 });
|
||||
res.image.close();
|
||||
}
|
||||
return decoder;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* AnimationManager - orchestrates rendering of animated images.
|
||||
*
|
||||
* Current Workflow
|
||||
* When IIPMetrics.imageType reports an animated image the IIP handler
|
||||
* stores the image in the storage and registers an animation of it.
|
||||
*
|
||||
* The animation manager works mostly self-contained:
|
||||
* - RAF loop
|
||||
* - 3 frame ahead decoding
|
||||
* - own bitmap ownership handling
|
||||
*
|
||||
* There are a few intersections with the ImageStorage and the ImageRenderer,
|
||||
* which still need to be consolidated (TODOs):
|
||||
* - ImageStorage: API for announcing memory state, better eviction strategy
|
||||
* - ImageRenderer: API for better drawing primitives
|
||||
*
|
||||
* In particular the ownership model of ImageSpec bitmaps (actual and orig)
|
||||
* needs a better story. We prolly need here an API to flip bitmaps.
|
||||
*/
|
||||
export class AnimationManager implements IDisposable, IResetHandler {
|
||||
private _animations = new Map<number, IAnimation>();
|
||||
private _animationFrame: number | undefined = undefined;
|
||||
private _draws: IImageDraw[] = [];
|
||||
|
||||
|
||||
constructor(
|
||||
private _storage: ImageStorage,
|
||||
private _renderer: ImageRenderer,
|
||||
private _terminal: ITerminalExt
|
||||
) {}
|
||||
|
||||
|
||||
private _requestAF(cb: FrameRequestCallback): number | undefined {
|
||||
return this._terminal._core._coreBrowserService?.window.requestAnimationFrame(cb);
|
||||
}
|
||||
|
||||
|
||||
private _cancelAF(id: number): void | undefined {
|
||||
return this._terminal._core._coreBrowserService?.window.cancelAnimationFrame(id);
|
||||
}
|
||||
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reset handler.
|
||||
* Stops all animations and frees all ressources.
|
||||
*/
|
||||
public reset(): void {
|
||||
for (const anim of this._animations.values()) {
|
||||
anim.inViewport = false;
|
||||
this._closeDecoder(anim);
|
||||
anim.cache.dispose();
|
||||
}
|
||||
this._animations.clear();
|
||||
this._draws = [];
|
||||
if (this._animationFrame) this._cancelAF(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Main hook to register an animated image for animation.
|
||||
*
|
||||
* @param id Image ID of the image storage the image was registered under.
|
||||
* @param data Raw data bytes of the image.
|
||||
* @param metrics Metrics of the image as returned by IIPMetrics.imageType.
|
||||
* @param w Output width in pixel.
|
||||
* @param h Output height in pixel.
|
||||
*/
|
||||
public async registerAnimation(id: number, data: Blob, metrics: IMetrics, w: number, h: number): Promise<void> {
|
||||
let decoder = await BlobDecoder(data, metrics.mime);
|
||||
const track = decoder.tracks.selectedTrack;
|
||||
if (!track || track.frameCount < 2) {
|
||||
decoder.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const frameCount = Math.min(track.frameCount, Constants.MAX_FRAMES);
|
||||
const frames: number[] = [];
|
||||
const timestamps: number[] = [];
|
||||
const durations: number[] = [];
|
||||
let elapsed = 0;
|
||||
|
||||
for (let i = 0; i < frameCount; i++) {
|
||||
try {
|
||||
const result = await decoder.decode({ frameIndex: i });
|
||||
const frame = result.image;
|
||||
const width = frame.displayWidth;
|
||||
const height = frame.displayHeight;
|
||||
const duration = (frame.duration ?? 0) / 1000;
|
||||
frame.close();
|
||||
// skip wrong dimensions and zero duration
|
||||
if (width !== metrics.width || height !== metrics.height || !duration) {
|
||||
if (durations.length) {
|
||||
durations[durations.length - 1] += duration;
|
||||
elapsed += duration;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
frames.push(i);
|
||||
timestamps.push(elapsed);
|
||||
durations.push(duration);
|
||||
elapsed += duration;
|
||||
} catch {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: still needs a consolidation of short frame durations:
|
||||
* We don't track v-sync, but just decode the next 3 follow-up frames.
|
||||
* This creates nonsense decoder pressure decoding frames,
|
||||
* that effectively never make it to the screen.
|
||||
* Currently our shortest time resolution is 1ms,
|
||||
* thus with 60 fps we would decode 15 1ms frames for no reason.
|
||||
* We prolly should introduce a lower bound of 8ms:
|
||||
* - in sync with 120 fps
|
||||
* - only every second frame is nonsense with 60 fps
|
||||
*/
|
||||
|
||||
if (frames.length < 2 || elapsed <= 0) {
|
||||
decoder.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isFirefox) {
|
||||
decoder.close();
|
||||
decoder = await BlobDecoder(data, metrics.mime);
|
||||
}
|
||||
|
||||
const anim: IAnimation = {
|
||||
id, data, w, h, decoder,
|
||||
metrics,
|
||||
numFrames: frames.length,
|
||||
decoderPromise: undefined,
|
||||
cache: new BitmapBuffer(),
|
||||
frames,
|
||||
startTime: undefined,
|
||||
current: 0,
|
||||
actual: 0,
|
||||
scheduled: false, // set by _schedule
|
||||
inViewport: true, // start with active prefetching
|
||||
highest: -1,
|
||||
playtime: elapsed,
|
||||
timestamps,
|
||||
frameIdx: 0
|
||||
};
|
||||
this._animations.set(id, anim);
|
||||
this.viewportUpdated();
|
||||
// early schedule to give decoder time to fill the cache before RAF
|
||||
this._schedule(anim);
|
||||
this._animationFrame ??= this._requestAF(this._loop);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove the animation for the given image id.
|
||||
*/
|
||||
public unregisterAnimation(id: number): void {
|
||||
const a = this._animations.get(id);
|
||||
if (a) {
|
||||
a.inViewport = false;
|
||||
this._closeDecoder(a);
|
||||
a.cache.dispose();
|
||||
this._animations.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* onRender handler.
|
||||
* Scratches the viewport buffer to collect draws for animation RAF
|
||||
* and to pause/resume invisible/visible animations.
|
||||
*/
|
||||
public viewportUpdated(_range?: { start: number, end: number }): void {
|
||||
const rows = this._terminal.rows;
|
||||
const cols = this._terminal.cols;
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const actives: Set<number> = new Set();
|
||||
this._draws = [];
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row + buffer.ydisp);
|
||||
if (!line) break;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
const e = line.getExtended(col)?.payload;
|
||||
if (e instanceof ImageTileInfo) {
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._storage.getImage(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line.getExtended(col)?.payload;
|
||||
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec && this._animations.has(imageId)) {
|
||||
this._draws.push({ imageId, imgSpec, tileId: startTile, col: startCol, row, count });
|
||||
actives.add(imageId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._updateAnimations(actives);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the frameIdx for elapsed time.
|
||||
* Note: The returned value is not the frame ID,
|
||||
* but the index for IAnimation.frames.
|
||||
*/
|
||||
private _timedFrameIdx(a: IAnimation, elapsed: number): number {
|
||||
elapsed %= a.playtime;
|
||||
let low = 0;
|
||||
let high = a.timestamps.length - 1;
|
||||
while (low <= high) {
|
||||
const mid = (low + high) >> 1;
|
||||
const startTime = a.timestamps[mid];
|
||||
if (elapsed < startTime) {
|
||||
high = mid - 1;
|
||||
} else {
|
||||
if (mid === a.timestamps.length - 1 || elapsed < a.timestamps[mid + 1]) {
|
||||
return mid;
|
||||
}
|
||||
low = mid + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pause/resume decoding from viewport changes.
|
||||
*/
|
||||
private _updateAnimations(visibles: Set<number>): void {
|
||||
for (const [id, anim] of this._animations) {
|
||||
const isVisible = visibles.has(id);
|
||||
if (isVisible && !anim.inViewport) {
|
||||
/**
|
||||
* There are two strategies possible here:
|
||||
*
|
||||
* 1. Resume decoding and pumping frames as soon as possible.
|
||||
* 2. Delay resuming further by a debounce to prevent flooding
|
||||
* new decoder instances on heavy scrolling.
|
||||
*
|
||||
* Currently the code below does 1. as the impact is rather
|
||||
* small from tests and gives the better user experience.
|
||||
*/
|
||||
anim.inViewport = true;
|
||||
const frameIdx = this._timedFrameIdx(anim, performance.now() - anim.startTime!);
|
||||
anim.frameIdx = frameIdx;
|
||||
anim.current = anim.frames[frameIdx];
|
||||
if (!anim.decoder && !anim.decoderPromise) {
|
||||
anim.decoderPromise = BlobDecoder(anim.data, anim.metrics.mime);
|
||||
}
|
||||
this._schedule(anim);
|
||||
} else if (!isVisible && anim.inViewport) {
|
||||
anim.inViewport = false;
|
||||
anim.cache.clear();
|
||||
this._closeDecoder(anim);
|
||||
}
|
||||
}
|
||||
if (visibles.size && this._animationFrame === undefined) {
|
||||
this._animationFrame = this._requestAF(this._loop);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* RAF for animations in viewport.
|
||||
*/
|
||||
private _loop = (ts: number): void => {
|
||||
if (!this._animations.size) {
|
||||
this._animationFrame = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
let mustRender = false;
|
||||
let anyVisible = false;
|
||||
|
||||
for (const anim of this._animations.values()) {
|
||||
if (!anim.inViewport) continue;
|
||||
|
||||
const spec = this._storage.getImage(anim.id);
|
||||
if (!spec) {
|
||||
this.unregisterAnimation(anim.id);
|
||||
continue;
|
||||
}
|
||||
anyVisible = true;
|
||||
anim.startTime ??= ts;
|
||||
const frameIdx = this._timedFrameIdx(anim, ts - anim.startTime);
|
||||
anim.frameIdx = frameIdx;
|
||||
const current = anim.frames[frameIdx];
|
||||
|
||||
if (current !== anim.current) {
|
||||
anim.current = current;
|
||||
const bm = anim.cache.pop(current);
|
||||
if (bm) {
|
||||
// FIXME: setting spec.actual delegates draws from onRender back to ImageStorage.render
|
||||
// --> should be avoided to prevent double draws within same AF
|
||||
// idea: create a mutex on image spec to tell storage.render to skip
|
||||
|
||||
// by swapping spec.actual we take ownership, thus have to close it here
|
||||
// but only if we have more than 4 frames (ringbuffer size is 4)
|
||||
// for <= 4 frames we move the bitmap back into cache
|
||||
// FIXME: create a bitmap swapping API in storage
|
||||
if (spec.actual instanceof ImageBitmap) {
|
||||
if (anim.numFrames > Constants.BUFFERSIZE) {
|
||||
spec.actual.close();
|
||||
} else {
|
||||
anim.cache.push(anim.actual, spec.actual);
|
||||
}
|
||||
}
|
||||
spec.actual = bm;
|
||||
anim.actual = current;
|
||||
mustRender = true;
|
||||
}
|
||||
this._schedule(anim);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: merge clears and draws into col x row rects
|
||||
if (mustRender) {
|
||||
for (let i = 0; i < this._draws.length; ++i) {
|
||||
const d = this._draws[i];
|
||||
this._renderer.clearLines(d.row, d.row);
|
||||
this._renderer.draw(d.imgSpec, d.tileId, d.col, d.row, d.count);
|
||||
}
|
||||
}
|
||||
this._animationFrame = anyVisible ? this._requestAF(this._loop) : undefined;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Try to schedule frame decoding.
|
||||
*/
|
||||
private _schedule(a: IAnimation): void {
|
||||
if (!a.scheduled) {
|
||||
let needsSchedule = false;
|
||||
// FIXME: do we need a == 4 test too?
|
||||
if (a.numFrames > 3) {
|
||||
needsSchedule =
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 1) % a.numFrames]) ||
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 2) % a.numFrames]) ||
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 3) % a.numFrames]);
|
||||
} else if (a.numFrames === 3) {
|
||||
needsSchedule =
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 1) % a.numFrames]) ||
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 2) % a.numFrames]);
|
||||
} else {
|
||||
needsSchedule =
|
||||
!a.cache.peek(a.frames[(a.frameIdx + 1) % a.numFrames]);
|
||||
}
|
||||
if (needsSchedule) {
|
||||
a.scheduled = true;
|
||||
setTimeout(() => this._decode(a), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decode up to next 3 frames.
|
||||
*/
|
||||
private async _decode(a: IAnimation): Promise<void> {
|
||||
if (!a.inViewport) {
|
||||
a.scheduled = false;
|
||||
return;
|
||||
}
|
||||
if (!a.decoder) {
|
||||
const decP = a.decoderPromise;
|
||||
a.decoderPromise = undefined;
|
||||
a.decoder = await (decP ?? BlobDecoder(a.data, a.metrics.mime));
|
||||
}
|
||||
const n1 = a.frames[(a.frameIdx + 1) % a.numFrames];
|
||||
const n2 = a.frames[(a.frameIdx + 2) % a.numFrames];
|
||||
const n3 = a.frames[(a.frameIdx + 3) % a.numFrames];
|
||||
for (const n of [n1, n2, n3]) {
|
||||
if (!a.cache.peek(n)) {
|
||||
if (isFirefox) {
|
||||
if (n <= a.highest) {
|
||||
a.highest = -1;
|
||||
a.decoder?.close();
|
||||
a.decoder = await BlobDecoder(a.data, a.metrics.mime);
|
||||
}
|
||||
a.highest = Math.max(n, a.highest);
|
||||
}
|
||||
let res: ImageDecodeResult;
|
||||
try {
|
||||
res = await a.decoder.decode({ frameIndex: n });
|
||||
} catch { continue; }
|
||||
let bm: ImageBitmap;
|
||||
try {
|
||||
// FIXME: we should not resize here yet
|
||||
// BETTER: rescale during draw:
|
||||
// - eases the GPU druing decoding
|
||||
// - rescales only what needs to be drawn
|
||||
// --> funneling down
|
||||
// FIXME: needs changes in image spec and rendering functions
|
||||
bm = await createImageBitmap(res.image, { resizeWidth: a.w, resizeHeight: a.h });
|
||||
} catch { continue; } finally { res?.image?.close(); }
|
||||
if (!a.inViewport) {
|
||||
bm.close();
|
||||
break;
|
||||
}
|
||||
a.cache.push(n, bm);
|
||||
}
|
||||
}
|
||||
a.scheduled = false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Close the decoder defensively.
|
||||
*/
|
||||
private _closeDecoder(a: IAnimation): void {
|
||||
const decoder = a.decoder;
|
||||
a.decoder = undefined;
|
||||
try { decoder?.close(); } catch {}
|
||||
const decP = a.decoderPromise;
|
||||
a.decoderPromise = undefined;
|
||||
decP?.then(dec => dec && dec !== decoder ? dec.close() : null).catch(() => {});
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,15 @@
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import type { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import type { IIPImageStorage } from './IIPImageStorage';
|
||||
import { CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
import type { AnimationManager } from 'AnimationManager';
|
||||
|
||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||
const enum DecoderConst {
|
||||
@@ -39,7 +40,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec: Base64Decoder;
|
||||
private _qoiDec: QoiDecoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
private _isMultipart = false;
|
||||
private _abortMulti = false;
|
||||
|
||||
@@ -47,7 +47,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: IIPImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
private readonly _coreTerminal: ITerminalExt,
|
||||
private readonly _aniManager: AnimationManager | undefined
|
||||
) {
|
||||
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
|
||||
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
|
||||
@@ -63,7 +64,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._metrics = UNSUPPORTED_TYPE;
|
||||
this._hp.reset();
|
||||
}
|
||||
|
||||
@@ -120,14 +120,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
|
||||
if (seqType === SequenceType.REPORTCELLSIZE) {
|
||||
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
||||
let width = CELL_SIZE_DEFAULT.width;
|
||||
let height = CELL_SIZE_DEFAULT.height;
|
||||
let w = CELL_SIZE_DEFAULT.width;
|
||||
let h = CELL_SIZE_DEFAULT.height;
|
||||
if (this._renderer.dimensions) {
|
||||
width = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
||||
height = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
||||
w = this._renderer.dimensions.css.canvas.width / this._coreTerminal.cols;
|
||||
h = this._renderer.dimensions.css.canvas.height / this._coreTerminal.rows;
|
||||
}
|
||||
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);
|
||||
return true;
|
||||
}
|
||||
@@ -154,17 +154,24 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
|
||||
// early exit condition chain
|
||||
let cond: number | boolean;
|
||||
let metrics = UNSUPPORTED_TYPE;
|
||||
if (cond = success) {
|
||||
if (cond = !this._dec.end()) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
metrics = imageType(this._dec.data8);
|
||||
if (cond = metrics.mime !== 'unsupported') {
|
||||
w = metrics.width;
|
||||
h = 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: image dimension issue ${metrics.width}x${metrics.height}`);
|
||||
}
|
||||
} else {
|
||||
console.warn('IIP: unsupported image type');
|
||||
}
|
||||
} else {
|
||||
console.warn('IIP: error during BASE64 decoding');
|
||||
}
|
||||
}
|
||||
if (!cond) {
|
||||
@@ -173,7 +180,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
}
|
||||
|
||||
let blob: Blob | ImageData;
|
||||
if (this._metrics.mime === 'image/qoi') {
|
||||
if (metrics.mime === 'image/qoi') {
|
||||
const data = this._qoiDec.decode(this._dec.data8);
|
||||
blob = new ImageData(
|
||||
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
|
||||
@@ -190,12 +197,20 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
||||
blob = new Blob([this._dec.data8], { type: metrics.mime });
|
||||
}
|
||||
this._dec.release();
|
||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
||||
.then(bm => {
|
||||
this._storage.addImage(bm);
|
||||
const id = this._storage.addImage(bm);
|
||||
if (this._aniManager && metrics.animated) {
|
||||
this._aniManager.registerAnimation(id, blob as Blob, metrics, w, h).catch(() => {});
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.catch(e => {
|
||||
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
|
||||
// FIXME: try ImageDecoder here instead (e.g. some AVIF work in Firefox)
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -222,8 +237,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
|
||||
private _dim(s: string, total: number, cdim: number): number {
|
||||
if (s === 'auto') return 0;
|
||||
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
|
||||
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
|
||||
return parseInt(s) * cdim;
|
||||
if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
|
||||
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
|
||||
return parseInt(s, 10) * cdim;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IAddImageOpts } from 'Types';
|
||||
import { IAddImageOpts } from './Types';
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export class IIPImageStorage {
|
||||
* Add an IIP image to storage.
|
||||
* Always uses scrolling mode — cursor advances past the image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, this._addImageOpts);
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): number {
|
||||
return this._storage.addImage(img, this._addImageOpts);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,26 +12,48 @@ const fs = require('fs');
|
||||
|
||||
|
||||
const TEST_IMAGES: [string, IMetrics][] = [
|
||||
['w3c_home_256.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_256.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_256.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home_2.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_2.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_2.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home_animation.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_gray.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_gray.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_gray.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home_noexif.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['spinfox.png', { mime: 'image/png', width: 148, height: 148 }],
|
||||
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }],
|
||||
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }],
|
||||
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
|
||||
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }],
|
||||
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }],
|
||||
['w3c_home_256.gif', { mime: 'image/gif', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_256.jpg', { mime: 'image/jpeg', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_256.png', { mime: 'image/png', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_2.gif', { mime: 'image/gif', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_2.jpg', { mime: 'image/jpeg', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_2.png', { mime: 'image/png', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_animation.gif', { mime: 'image/gif', width: 72, height: 48, animated: true }],
|
||||
['w3c_home.gif', { mime: 'image/gif', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_gray.gif', { mime: 'image/gif', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_gray.jpg', { mime: 'image/jpeg', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_gray.png', { mime: 'image/png', width: 72, height: 48, animated: false }],
|
||||
['w3c_home.jpg', { mime: 'image/jpeg', width: 72, height: 48, animated: false }],
|
||||
['w3c_home.png', { mime: 'image/png', width: 72, height: 48, animated: false }],
|
||||
['w3c_home_noexif.jpg', { mime: 'image/jpeg', width: 72, height: 48, animated: false }],
|
||||
['spinfox.png', { mime: 'image/png', width: 148, height: 148, animated: true }],
|
||||
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448, animated: false }],
|
||||
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600, animated: false }],
|
||||
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8, animated: false }],
|
||||
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000, animated: false }],
|
||||
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600, animated: false }],
|
||||
// VP8
|
||||
['1.webp', { mime: 'image/webp', width: 550, height: 368, animated: false }],
|
||||
['2.webp', { mime: 'image/webp', width: 550, height: 404, animated: false }],
|
||||
['3.webp', { mime: 'image/webp', width: 1280, height: 720, animated: false }],
|
||||
['4.webp', { mime: 'image/webp', width: 1024, height: 772, animated: false }],
|
||||
['5.webp', { mime: 'image/webp', width: 1024, height: 752, animated: false }],
|
||||
// VP8X
|
||||
['1_webp_a.webp', { mime: 'image/webp', width: 400, height: 301, animated: false }],
|
||||
['2_webp_a.webp', { mime: 'image/webp', width: 386, height: 395, animated: false }],
|
||||
['3_webp_a.webp', { mime: 'image/webp', width: 800, height: 600, animated: false }],
|
||||
['4_webp_a.webp', { mime: 'image/webp', width: 421, height: 163, animated: false }],
|
||||
['5_webp_a.webp', { mime: 'image/webp', width: 300, height: 300, animated: false }],
|
||||
// VP8L
|
||||
['1_webp_ll.webp', { mime: 'image/webp', width: 400, height: 301, animated: false }],
|
||||
['2_webp_ll.webp', { mime: 'image/webp', width: 386, height: 395, animated: false }],
|
||||
['3_webp_ll.webp', { mime: 'image/webp', width: 800, height: 600, animated: false }],
|
||||
['4_webp_ll.webp', { mime: 'image/webp', width: 421, height: 163, animated: false }],
|
||||
['5_webp_ll.webp', { mime: 'image/webp', width: 300, height: 300, animated: false }],
|
||||
// some AVIF test images
|
||||
['fox.profile2.12bpc.yuv444.odd-width.odd-height.avif', { mime: 'image/avif', width: 1203, height: 799, animated: false }],
|
||||
['hato.profile0.8bpc.yuv420.avif', { mime: 'image/avif', width: 3082, height: 2048, animated: false }],
|
||||
['kimono.crop.avif', { mime: 'image/avif', width: 722, height: 1024, animated: false }],
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -4,62 +4,140 @@
|
||||
*/
|
||||
|
||||
|
||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'unsupported' | '';
|
||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/qoi' | 'image/webp' | 'image/avif' | 'unsupported' | '';
|
||||
|
||||
export interface IMetrics {
|
||||
mime: ImageType;
|
||||
width: number;
|
||||
height: number;
|
||||
animated: boolean;
|
||||
}
|
||||
|
||||
export const UNSUPPORTED_TYPE: IMetrics = {
|
||||
mime: 'unsupported',
|
||||
width: 0,
|
||||
height: 0
|
||||
height: 0,
|
||||
animated: false
|
||||
};
|
||||
|
||||
export function imageType(d: Uint8Array): IMetrics {
|
||||
if (d.length < 24) {
|
||||
if (d.length < 32) {
|
||||
return UNSUPPORTED_TYPE;
|
||||
}
|
||||
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
|
||||
const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
||||
// + 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]
|
||||
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
||||
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23],
|
||||
animated: isAPNG(d)
|
||||
};
|
||||
}
|
||||
// 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 };
|
||||
return { mime: 'image/jpeg', width, height, animated: false };
|
||||
}
|
||||
// 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]
|
||||
width: d[6] | d[7] << 8,
|
||||
height: d[8] | d[9] << 8,
|
||||
animated: isAGIF(d)
|
||||
};
|
||||
}
|
||||
// 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]
|
||||
width: d[4] << 24 | d[5] << 16 | d[ 6] << 8 | d[ 7],
|
||||
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11],
|
||||
animated: false
|
||||
};
|
||||
}
|
||||
// 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,
|
||||
animated: (d[20] & 0x02) !== 0
|
||||
};
|
||||
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,
|
||||
animated: false
|
||||
};
|
||||
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,
|
||||
animated: false
|
||||
};
|
||||
}
|
||||
return UNSUPPORTED_TYPE;
|
||||
}
|
||||
// AVIF: Box size | ftyp | avif/avis
|
||||
if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
|
||||
let pos = -1;
|
||||
let anim = d32[2] === 0x73697661; // avis is meant to be animated
|
||||
// search for boxes within first 1024 bytes
|
||||
const limit = Math.min(d.length - 16, 1024);
|
||||
for (let i = 8; i < limit; i++) {
|
||||
// scan for ispe for dimensions
|
||||
if (pos === -1 && d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
|
||||
pos = i;
|
||||
}
|
||||
// scan for stsz or moov
|
||||
if (!anim) {
|
||||
const isStsz = d[i] === 0x73 && d[i + 1] === 0x74 && d[i + 2] === 0x73 && d[i + 3] === 0x7A;
|
||||
const isMoov = d[i] === 0x6D && d[i + 1] === 0x6F && d[i + 2] === 0x6F && d[i + 3] === 0x76;
|
||||
if (isStsz || isMoov) {
|
||||
anim = true;
|
||||
}
|
||||
}
|
||||
if (pos !== -1 && anim) {
|
||||
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, animated: anim };
|
||||
}
|
||||
}
|
||||
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];
|
||||
if (blockLength < 2) return [0, 0];
|
||||
while (true) {
|
||||
i += blockLength;
|
||||
if (i >= len) {
|
||||
@@ -80,5 +158,78 @@ function jpgSize(d: Uint8Array): [number, number] {
|
||||
}
|
||||
i += 2;
|
||||
blockLength = d[i] << 8 | d[i + 1];
|
||||
if (blockLength < 2) return [0, 0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isAGIF(data: Uint8Array): boolean {
|
||||
let pos = 10;
|
||||
const gPacked = data[pos];
|
||||
const hasGP = (gPacked & 0x80) !== 0;
|
||||
pos += 3;
|
||||
if (hasGP) {
|
||||
const gpSize = 2 << (gPacked & 0x07);
|
||||
pos += 3 * gpSize;
|
||||
}
|
||||
let gceCount = 0;
|
||||
while (pos < data.length) {
|
||||
const blockType = data[pos];
|
||||
if (blockType === 0x21) { // Extension Block
|
||||
if (pos + 1 >= data.length) return false;
|
||||
const extLabel = data[pos + 1];
|
||||
if (extLabel === 0xF9) { // Graphic Control Extension
|
||||
gceCount++;
|
||||
if (gceCount > 1) return true; // early exit on 2nd frame
|
||||
}
|
||||
pos += 2;
|
||||
while (pos < data.length) {
|
||||
const blockSize = data[pos];
|
||||
if (blockSize === 0x00) break;
|
||||
pos += blockSize + 1;
|
||||
}
|
||||
if (pos >= data.length) return false;
|
||||
pos++;
|
||||
} else if (blockType === 0x2C) { // Image Descriptor
|
||||
if (pos + 10 >= data.length) return false;
|
||||
pos += 10;
|
||||
const lPacked = data[pos - 1];
|
||||
if ((lPacked & 0x80) !== 0) {
|
||||
const lpSize = 2 << (lPacked & 0x07);
|
||||
pos += 3 * lpSize;
|
||||
}
|
||||
if (pos >= data.length) return false;
|
||||
pos++;
|
||||
while (pos < data.length) {
|
||||
const blockSize = data[pos];
|
||||
if (blockSize === 0x00) break;
|
||||
pos += blockSize + 1;
|
||||
}
|
||||
if (pos >= data.length) return false;
|
||||
pos++;
|
||||
} else if (blockType === 0x3B) {
|
||||
break;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function isAPNG(d: Uint8Array): boolean {
|
||||
let p = 8;
|
||||
while (p + 8 < d.length) {
|
||||
const length = (d[p] << 24 | d[p + 1] << 16 | d[p + 2] << 8 | d[p + 3]) >>> 0;
|
||||
// 'acTL' before 'IDAT': APNG
|
||||
if (d[p + 4] === 0x61 && d[p + 5] === 0x63 && d[p + 6] === 0x54 && d[p + 7] === 0x4c) {
|
||||
return true;
|
||||
}
|
||||
// 'IDAT' first: no APNG
|
||||
if (d[p + 4] === 0x49 && d[p + 5] === 0x44 && d[p + 6] === 0x41 && d[p + 7] === 0x54) {
|
||||
return false;
|
||||
}
|
||||
p += 12 + length;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { SixelHandler } from './SixelHandler';
|
||||
import { SixelImageStorage } from './SixelImageStorage';
|
||||
import { IIPImageStorage } from './IIPImageStorage';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
import { AnimationManager } from './AnimationManager';
|
||||
|
||||
|
||||
/**
|
||||
@@ -70,7 +71,9 @@ const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 33554432,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 33554432
|
||||
kittySizeLimit: 33554432,
|
||||
animationSupport: true, // alpha
|
||||
animationPixelLimit: 4194304 // limit to 2048 * 2048 pixels
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
@@ -104,6 +107,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _terminal: ITerminalExt | undefined;
|
||||
private _handlers: Map<String, IResetHandler> = new Map();
|
||||
private _aniManager: AnimationManager | undefined;
|
||||
private readonly _onImageAdded = new Emitter<void>();
|
||||
public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
|
||||
|
||||
@@ -144,6 +148,11 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
terminal.options.windowOptions = windowOps;
|
||||
}
|
||||
|
||||
if (this._opts.animationSupport && typeof ImageDecoder === 'function') {
|
||||
this._aniManager = new AnimationManager(this._storage, this._renderer, terminal);
|
||||
this._disposeLater(terminal.onRender(range => this._aniManager!.viewportUpdated(range)));
|
||||
}
|
||||
|
||||
this._disposeLater(
|
||||
this._renderer,
|
||||
this._storage,
|
||||
@@ -187,7 +196,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
// iTerm IIP handler
|
||||
if (this._opts.iipSupport) {
|
||||
const iipStorage = new IIPImageStorage(this._storage!);
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal);
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, iipStorage, terminal, this._aniManager);
|
||||
this._handlers.set('iip', iipHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
@@ -218,6 +227,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
for (const handler of this._handlers.values()) {
|
||||
handler.reset();
|
||||
}
|
||||
this._aniManager?.reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,11 @@
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import {
|
||||
ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
|
||||
IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
|
||||
UnderlineStyle, IAddImageOpts
|
||||
import type {
|
||||
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts
|
||||
} from './Types';
|
||||
|
||||
import type { IBufferLine } from 'common/buffer/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
|
||||
// fallback default cell size
|
||||
export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||
@@ -18,93 +17,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||
height: 14
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export class ImageTileInfo {
|
||||
constructor(
|
||||
ext: number = 0,
|
||||
urlId: number = 0,
|
||||
public imageId = -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;
|
||||
public tileId = -1) {
|
||||
}
|
||||
}
|
||||
const EMPTY_ATTRS = new ExtendedAttrsImage();
|
||||
|
||||
|
||||
/**
|
||||
* ImageStorage - extension of CoreTerminal:
|
||||
@@ -126,6 +44,7 @@ export class ImageStorage implements IDisposable {
|
||||
private _needsFullClear = false;
|
||||
// hard limit of stored pixels (fallback limit of 10 MB)
|
||||
private _pixelLimit: number = 2500000;
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _viewportMetrics: { cols: number, rows: number };
|
||||
public onImageAdded: (() => void) | undefined;
|
||||
@@ -224,6 +143,13 @@ export class ImageStorage implements IDisposable {
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve image spec from storage.
|
||||
*/
|
||||
public getImage(id: number): IImageSpec | undefined {
|
||||
return this._images.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an image by its internal storage ID.
|
||||
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
|
||||
@@ -277,10 +203,10 @@ export class ImageStorage implements IDisposable {
|
||||
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
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) {
|
||||
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++;
|
||||
}
|
||||
if (opts.scrolling) {
|
||||
@@ -427,55 +353,44 @@ export class ImageStorage implements IDisposable {
|
||||
const placeholderCalls: { col: number, row: number, count: number }[] = [];
|
||||
|
||||
// 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) {
|
||||
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
||||
const line = buffer.lines.get(row + buffer.ydisp);
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
let e: IExtendedAttrsImage;
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
e = line._extendedAttrs[col] ?? EMPTY_ATTRS;
|
||||
} else {
|
||||
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
|
||||
const e = line.getExtended(col)?.payload;
|
||||
if (e instanceof ImageTileInfo) {
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
e = maybeImg;
|
||||
}
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined;
|
||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
*/
|
||||
while (++col < cols) {
|
||||
const nextE = line.getExtended(col)?.payload;
|
||||
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
placeholderCalls.push({ col: startCol, row, count });
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
placeholderCalls.push({ col: startCol, row, count });
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -513,9 +428,9 @@ export class ImageStorage implements IDisposable {
|
||||
const rows = buffer.lines.length;
|
||||
const oldCol = this._viewportMetrics.cols - 1;
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS;
|
||||
const line = buffer.lines.get(row)!;
|
||||
const e = line.getExtended(oldCol)?.payload;
|
||||
if (e instanceof ImageTileInfo) {
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
@@ -532,7 +447,7 @@ export class ImageStorage implements IDisposable {
|
||||
// expand only if right side is empty (nothing got wrapped from below)
|
||||
let hasData = false;
|
||||
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;
|
||||
break;
|
||||
}
|
||||
@@ -544,7 +459,7 @@ export class ImageStorage implements IDisposable {
|
||||
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
||||
let lastTile = e.tileId;
|
||||
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
||||
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
|
||||
this._writeToCell(line, expandCol, imageId, ++lastTile);
|
||||
imgSpec.tileCount++;
|
||||
}
|
||||
}
|
||||
@@ -558,10 +473,10 @@ export class ImageStorage implements IDisposable {
|
||||
*/
|
||||
public getImageAtBufferCell(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) {
|
||||
const line = buffer.lines.get(y);
|
||||
if (line) {
|
||||
const e = line.getExtended(x)?.payload;
|
||||
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
|
||||
const orig = this._images.get(e.imageId)?.orig;
|
||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
||||
@@ -578,10 +493,10 @@ export class ImageStorage implements IDisposable {
|
||||
*/
|
||||
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 line = buffer.lines.get(y);
|
||||
if (line) {
|
||||
const e = line.getExtended(x)?.payload;
|
||||
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||
const spec = this._images.get(e.imageId);
|
||||
if (spec) {
|
||||
return this._renderer.extractTile(spec, e.tileId);
|
||||
@@ -609,31 +524,31 @@ export class ImageStorage implements IDisposable {
|
||||
return used - current;
|
||||
}
|
||||
|
||||
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const old = line._extendedAttrs[x];
|
||||
if (old) {
|
||||
if (old.imageId !== undefined) {
|
||||
// found an old ExtendedAttrsImage, since we know that
|
||||
// they are always isolated instances (single cell usage),
|
||||
// we can re-use it and just update their id entries
|
||||
const oldSpec = this._images.get(old.imageId);
|
||||
if (oldSpec) {
|
||||
// early eviction for in-viewport overwrites
|
||||
oldSpec.tileCount--;
|
||||
}
|
||||
old.imageId = imageId;
|
||||
old.tileId = tileId;
|
||||
return;
|
||||
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void {
|
||||
const workCell = this._workCell;
|
||||
line.loadCell(x, workCell);
|
||||
if (this._workCell.hasExtendedAttrs()) {
|
||||
const old = workCell.extended.payload;
|
||||
if (old instanceof ImageTileInfo) {
|
||||
// found an old ExtendedAttrsImage, since we know that
|
||||
// they are always isolated instances (single cell usage),
|
||||
// we can re-use it and just update their id entries
|
||||
const oldSpec = this._images.get(old.imageId);
|
||||
if (oldSpec) {
|
||||
// early eviction for in-viewport overwrites
|
||||
oldSpec.tileCount--;
|
||||
}
|
||||
// found a plain ExtendedAttrs instance, clone it to new entry
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
||||
old.imageId = imageId;
|
||||
old.tileId = tileId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fall-through: always create new ExtendedAttrsImage entry
|
||||
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
||||
// fall-through: always create new ExtendedAttrs entry
|
||||
const extattr = workCell.extended.clone();
|
||||
extattr.payload = new ImageTileInfo(imageId, tileId);
|
||||
workCell.extended = extattr;
|
||||
workCell.updateExtended();
|
||||
line.setCell(x, workCell);
|
||||
}
|
||||
|
||||
private _evictOnAlternate(): void {
|
||||
@@ -646,18 +561,16 @@ export class ImageStorage implements IDisposable {
|
||||
// re-count tiles on whole buffer
|
||||
const buffer = this._terminal._core.buffer;
|
||||
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) {
|
||||
continue;
|
||||
}
|
||||
for (let x = 0; x < this._terminal.cols; ++x) {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const imgId = line._extendedAttrs[x]?.imageId;
|
||||
if (imgId) {
|
||||
const spec = this._images.get(imgId);
|
||||
if (spec) {
|
||||
spec.tileCount++;
|
||||
}
|
||||
const payload = line.getExtended(x)?.payload;
|
||||
if (payload instanceof ImageTileInfo) {
|
||||
const spec = this._images.get(payload.imageId);
|
||||
if (spec) {
|
||||
spec.tileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
|
||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'target-browser/Types';
|
||||
import type { IRenderDimensions } from 'target-browser/renderer/shared/Types';
|
||||
import type { ICoreBrowserService, IRenderService, IThemeService } from 'target-browser/services/Services';
|
||||
import type { IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
|
||||
export const enum Cell {
|
||||
CONTENT = 0, // codepoint and wcwidth information (enum Content)
|
||||
@@ -40,6 +40,8 @@ export interface IImageAddonOptions {
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
animationSupport: boolean;
|
||||
animationPixelLimit: number;
|
||||
}
|
||||
|
||||
export interface IResetHandler {
|
||||
@@ -47,6 +49,7 @@ export interface IResetHandler {
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Stub into private interfaces.
|
||||
* This should be kept in line with common libs.
|
||||
@@ -54,19 +57,6 @@ export interface IResetHandler {
|
||||
* 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 {
|
||||
_parser: IEscapeSequenceParser;
|
||||
_curAttrData: AttributeData;
|
||||
@@ -77,6 +67,7 @@ interface IInputHandlerExt extends IInputHandler {
|
||||
};
|
||||
onRequestReset(handler: () => void): IDisposable;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
export interface ICoreTerminalExt extends ITerminal {
|
||||
_themeService: IThemeService | undefined;
|
||||
@@ -88,8 +79,6 @@ export interface ICoreTerminalExt extends ITerminal {
|
||||
export interface ITerminalExt extends Terminal {
|
||||
_core: ICoreTerminalExt;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
|
||||
/**
|
||||
* Some storage definitions.
|
||||
|
||||
@@ -168,7 +168,7 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value);
|
||||
const numValue = parseInt(value, 10);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
case KittyKey.ID: cmd.id = numValue; break;
|
||||
|
||||
@@ -12,11 +12,8 @@
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"target-browser/*": [ "../../../src/target-browser/*" ],
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
],
|
||||
"browser/*": [ "../../../src/browser/*" ],
|
||||
"common/*": [ "../../../src/common/*" ],
|
||||
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
|
||||
"*": [ "./*" ]
|
||||
}
|
||||
@@ -26,7 +23,7 @@
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/target-browser" },
|
||||
{ "path": "../../../src" }
|
||||
{ "path": "../../../src/browser" },
|
||||
{ "path": "../../../src/common" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ export interface IImageAddonOptions {
|
||||
iipSizeLimit: number;
|
||||
kittySupport: boolean;
|
||||
kittySizeLimit: number;
|
||||
animationSupport: boolean;
|
||||
animationPixelLimit: number;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -138,7 +140,9 @@ test.describe('ImageAddon', () => {
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 33554432,
|
||||
kittySupport: true,
|
||||
kittySizeLimit: 33554432
|
||||
kittySizeLimit: 33554432,
|
||||
animationSupport: true,
|
||||
animationPixelLimit: 4194304
|
||||
};
|
||||
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
@@ -155,7 +159,9 @@ test.describe('ImageAddon', () => {
|
||||
iipSupport: false,
|
||||
iipSizeLimit: 1000,
|
||||
kittySupport: false,
|
||||
kittySizeLimit: 1000
|
||||
kittySizeLimit: 1000,
|
||||
animationSupport: false,
|
||||
animationPixelLimit: 1000
|
||||
};
|
||||
await ctx.page.evaluate(opts => {
|
||||
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
|
||||
@@ -435,6 +441,15 @@ test.describe('ImageAddon', () => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe('text overwrite removes tiles', () => {
|
||||
test('sixel', async () => {
|
||||
await assertTextClearsTiles(SIXEL_SEQ_0);
|
||||
});
|
||||
test('iip', async () => {
|
||||
await assertTextClearsTiles(TESTDATA_IIP[0][0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -472,3 +487,20 @@ async function getOrigSize(id: number): Promise<[number, number]> {
|
||||
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
|
||||
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
|
||||
}
|
||||
|
||||
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
||||
}
|
||||
|
||||
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
|
||||
await ctx.proxy.write('\x1b[H' + imageSeq);
|
||||
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
||||
ok(await hasTileAtBufferCell(0, 1));
|
||||
await ctx.proxy.write('\x1b[2;6H#######');
|
||||
for (let x = 5; x < 12; x++) {
|
||||
strictEqual(await hasTileAtBufferCell(x, 1), false);
|
||||
}
|
||||
ok(await hasTileAtBufferCell(0, 1));
|
||||
ok(await hasTileAtBufferCell(13, 1));
|
||||
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
|
||||
}
|
||||
|
||||
@@ -2213,6 +2213,21 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await pollFor(ctx.page, 'window._imageAddedCount', 2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('text overwrite removes tiles', () => {
|
||||
test('a=T placement', async () => {
|
||||
await ctx.proxy.write(`\x1b[H\x1b_Ga=T,f=100;${KITTY_MULTICOLOR_200X100_BASE64}\x1b\\`);
|
||||
await pollFor(ctx.page, '!!window.imageAddon.extractTileAtBufferCell(5, 1)', true);
|
||||
ok(await hasTileAtBufferCell(0, 1));
|
||||
await ctx.proxy.write('\x1b[2;6H#######');
|
||||
for (let x = 5; x < 12; x++) {
|
||||
strictEqual(await hasTileAtBufferCell(x, 1), false);
|
||||
}
|
||||
ok(await hasTileAtBufferCell(0, 1));
|
||||
ok(await hasTileAtBufferCell(13, 1));
|
||||
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -2262,3 +2277,7 @@ async function getPixels(col: number, row: number, x: number, y: number, w: numb
|
||||
return Array.from(ctx2d.getImageData(x, y, w, h).data);
|
||||
}, [col, row, x, y, w, h]);
|
||||
}
|
||||
|
||||
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
|
||||
return ctx.page.evaluate(`!!window.imageAddon.extractTileAtBufferCell(${x}, ${y})`);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,11 +25,8 @@ const addon = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
browser: path.resolve('../../out/target-browser'),
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser'),
|
||||
vs: path.resolve('../../out/vs')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@ const CACHE_SIZE = 100000;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* start to render them.
|
||||
* @param term Terminal instance from xterm.js
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
],
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-progress": [
|
||||
"../typings/addon-progress.d.ts"
|
||||
@@ -33,10 +32,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,11 +25,8 @@ module.exports = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
browser: path.resolve('../../out/target-browser'),
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser'),
|
||||
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;
|
||||
results.push(prevResult);
|
||||
result = this._engine.find(
|
||||
term,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
|
||||
searchOptions
|
||||
);
|
||||
const cols = this._terminal.cols;
|
||||
let nextCol = prevResult.col + prevResult.size;
|
||||
let nextRow = prevResult.row;
|
||||
if (nextCol >= cols) {
|
||||
nextRow += Math.floor(nextCol / cols);
|
||||
nextCol = nextCol % cols;
|
||||
}
|
||||
result = this._engine.find(term, nextRow, nextCol, searchOptions);
|
||||
}
|
||||
|
||||
this._resultTracker.updateResults(results, this._highlightLimit);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { assert } from 'chai';
|
||||
import { SearchEngine } from './SearchEngine';
|
||||
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 { DisposableStore } from 'common/Lifecycle';
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ export class SearchEngine {
|
||||
this._terminal.clearSelection();
|
||||
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`);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { SearchLineCache, LineCacheEntry } from './SearchLineCache';
|
||||
import { Terminal } from 'target-browser/public/Terminal';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { timeout } from 'common/Async';
|
||||
|
||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||
|
||||
@@ -16,11 +16,10 @@
|
||||
],
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-search" : [
|
||||
"../typings/addon-search.d.ts"
|
||||
@@ -33,10 +32,10 @@
|
||||
],
|
||||
"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('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', () => {
|
||||
let fixture: string;
|
||||
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') {
|
||||
fixture = fixture.replace(/\n/g, '\n\r');
|
||||
}
|
||||
@@ -501,7 +515,7 @@ test.describe('Search Tests', () => {
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
// 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);
|
||||
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,10 +25,7 @@ module.exports = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
common: path.resolve('../../out/common'),
|
||||
vs: path.resolve('../../out/vs')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
||||
|
||||
import { spawn } from 'node-pty';
|
||||
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { Terminal } from 'target-browser/public/Terminal';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { SerializeAddon } from 'SerializeAddon';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
|
||||
@@ -12,11 +12,8 @@
|
||||
"target": "es2021",
|
||||
"module": "nodenext",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
],
|
||||
"target-browser/*": ["../../../src/target-browser/*"],
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
"SerializeAddon": ["../src/SerializeAddon"],
|
||||
"@xterm/addon-serialize": [
|
||||
"../typings/addon-serialize.d.ts"
|
||||
@@ -26,7 +23,7 @@
|
||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||
"references": [
|
||||
{ "path": "../../../src" },
|
||||
{ "path": "../../../src/target-browser" }
|
||||
{ "path": "../../../src/common" },
|
||||
{ "path": "../../../src/browser" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start",
|
||||
"benchmark": "NODE_PATH=../../out:./out:./out-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-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": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
|
||||
"benchmark-baseline": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline 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 { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'target-browser/public/Terminal';
|
||||
import { SelectionModel } from 'target-browser/selection/SelectionModel';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { ThemeService } from 'target-browser/services/ThemeService';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
|
||||
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
import { DEFAULT_ANSI_COLORS } from 'target-browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IAttributeData } from 'common/buffer/Types';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
@@ -172,7 +173,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
||||
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
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// 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)
|
||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||
// 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)
|
||||
) {
|
||||
isValid = true;
|
||||
@@ -232,7 +233,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
isNextRowFirstCharDoubleWidth &&
|
||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||
// 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(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
|
||||
@@ -12,11 +12,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"@xterm/addon-serialize": [
|
||||
"../typings/addon-serialize.d.ts"
|
||||
@@ -34,10 +33,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ test.describe('SerializeAddon', () => {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
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(BOLD) + line,
|
||||
sgr(UNDERLINED) + line,
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
@@ -29,10 +28,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,11 +25,8 @@ module.exports = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
browser: path.resolve('../../out/target-browser')
|
||||
common: path.resolve('../../out/common'),
|
||||
browser: path.resolve('../../out/browser')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -7,7 +7,7 @@ import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
||||
|
||||
import { spawn } from 'node-pty';
|
||||
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { CoreBrowserTerminal } from 'target-browser/CoreBrowserTerminal';
|
||||
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
|
||||
import { UnicodeGraphemeProvider } from 'UnicodeGraphemeProvider';
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,8 @@
|
||||
"target": "es2015",
|
||||
"module": "nodenext",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
],
|
||||
"target-browser/*": ["../../../src/target-browser/*"],
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
"UnicodeGraphemeProvider": ["../src/UnicodeGraphemeProvider"],
|
||||
"@xterm/addon-unicode-graphemes": [
|
||||
"../typings/addon-unicode-graphemes.d.ts"
|
||||
@@ -23,7 +20,7 @@
|
||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||
"references": [
|
||||
{ "path": "../../../src" },
|
||||
{ "path": "../../../src/target-browser" }
|
||||
{ "path": "../../../src/common" },
|
||||
{ "path": "../../../src/browser" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"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-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-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": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json 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_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 { createPropertyValue, extractCharKind, extractWidth } from 'common/input/UnicodeProperties';
|
||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/input/UnicodeTypes';
|
||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services';
|
||||
import { UnicodeService } from 'common/services/UnicodeService';
|
||||
import * as UC from './third-party/UnicodeProperties';
|
||||
|
||||
export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
||||
@@ -19,7 +19,7 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
||||
}
|
||||
|
||||
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 {
|
||||
// Optimize the simple ASCII case, under the condition that
|
||||
@@ -39,9 +39,9 @@ export class UnicodeGraphemeProvider implements IUnicodeVersionProvider {
|
||||
w = 1;
|
||||
}
|
||||
if (preceding !== 0) {
|
||||
const oldWidth = extractWidth(preceding);
|
||||
const oldWidth = UnicodeService.extractWidth(preceding);
|
||||
if (this.handleGraphemes) {
|
||||
charInfo = UC.shouldJoin(extractCharKind(preceding), charInfo);
|
||||
charInfo = UC.shouldJoin(UnicodeService.extractCharKind(preceding), charInfo);
|
||||
} else {
|
||||
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 {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* UnicodeVersionProvider for V15 with grapeme cluster handleing.
|
||||
* UnicodeVersionProvider for V15 with grapheme cluster handling.
|
||||
*/
|
||||
|
||||
import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"@xterm/addon-unicode-graphemes": [
|
||||
"../typings/addon-unicode-graphemes.d.ts"
|
||||
@@ -30,7 +29,7 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
@@ -29,10 +28,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,10 +25,7 @@ module.exports = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
common: path.resolve('../../out/common'),
|
||||
vs: path.resolve('../../out/vs')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { IUnicodeVersionProvider } from '@xterm/xterm';
|
||||
import { createPropertyValue, extractWidth } from 'common/input/UnicodeProperties';
|
||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/input/UnicodeTypes';
|
||||
import { UnicodeCharProperties, UnicodeCharWidth } from 'common/services/Services';
|
||||
import { UnicodeService } from 'common/services/UnicodeService';
|
||||
|
||||
const BMP_COMBINING = [
|
||||
[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD],
|
||||
@@ -223,13 +223,13 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
|
||||
let width = this.wcwidth(codepoint);
|
||||
let shouldJoin = width === 0 && preceding !== 0;
|
||||
if (shouldJoin) {
|
||||
const oldWidth = extractWidth(preceding);
|
||||
const oldWidth = UnicodeService.extractWidth(preceding);
|
||||
if (oldWidth === 0) {
|
||||
shouldJoin = false;
|
||||
} else if (oldWidth > width) {
|
||||
width = oldWidth;
|
||||
}
|
||||
}
|
||||
return createPropertyValue(0, width, shouldJoin);
|
||||
return UnicodeService.createPropertyValue(0, width, shouldJoin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,7 @@
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"@xterm/addon-unicode11": [
|
||||
"../typings/addon-unicode11.d.ts"
|
||||
@@ -33,7 +32,7 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -25,10 +25,7 @@ module.exports = {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: [
|
||||
path.resolve('../../out/runtime'),
|
||||
path.resolve('../../out/primitives'),
|
||||
],
|
||||
common: path.resolve('../../out/common'),
|
||||
vs: path.resolve('../../out/vs')
|
||||
}
|
||||
},
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -57,7 +57,8 @@ function isUrl(urlString: string): boolean {
|
||||
|
||||
export class LinkComputer {
|
||||
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 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 { 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
|
||||
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
||||
// resembling the old (...)*([^:"\'\\s]) final path clause
|
||||
// additionally exclude early + final:
|
||||
// - 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:
|
||||
// - final interpunction like ,.!?
|
||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
"removeComments": true,
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/primitives/*",
|
||||
"../../../src/runtime/*",
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"target-browser/*": [
|
||||
"../../../src/target-browser/*"
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
],
|
||||
"*": [
|
||||
"./*"
|
||||
@@ -32,10 +31,10 @@
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src"
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/target-browser"
|
||||
"path": "../../../src/browser"
|
||||
},
|
||||
{
|
||||
"path": "../../../test/playwright"
|
||||
|
||||
@@ -23,7 +23,7 @@ See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-
|
||||
|
||||
### 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
|
||||
const terminal = new Terminal();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ISelectionRenderModel } from 'target-browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'target-browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'target-browser/Types';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { ICellData } from 'common/buffer/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { rgba } from 'common/Color';
|
||||
import { treatGlyphAsBackgroundColor } from 'target-browser/renderer/shared/RendererUtils';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
import { blockPatternCodepoints } from './customGlyphs/CustomGlyphDefinitions';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { TextureAtlas } from './TextureAtlas';
|
||||
import { ITerminalOptions, Terminal } from '@xterm/xterm';
|
||||
import { ITerminal, ReadonlyColorSet } from 'target-browser/Types';
|
||||
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ICharAtlasConfig, ITextureAtlas } from './Types';
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { ITerminalOptions } from '@xterm/xterm';
|
||||
import { IColorSet, ReadonlyColorSet } from 'target-browser/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig {
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { RendererConstants } from 'target-browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService } from 'target-browser/services/Services';
|
||||
import { RendererConstants } from 'browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { TextureAtlas } from './TextureAtlas';
|
||||
import { IRenderDimensions } from 'target-browser/renderer/shared/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject, type IRasterizedGlyph, type ITextureAtlas } from './Types';
|
||||
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import type { ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { allowRescaling, throwIfFalsy } from 'target-browser/renderer/shared/RendererUtils';
|
||||
import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
@@ -100,6 +100,8 @@ export class GlyphRenderer extends Disposable {
|
||||
private readonly _attributesBuffer: WebGLBuffer;
|
||||
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
private _lastSeenPageLayoutVersion: number = -1;
|
||||
private _pageOverflowWarned: boolean = false;
|
||||
private _activeBuffer: number = 0;
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
@@ -213,14 +215,25 @@ export class GlyphRenderer extends Disposable {
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call when a frame is being drawn. Returns whether the full model must be rebuilt before
|
||||
* rendering this frame because the atlas page layout changed since this renderer last drew.
|
||||
*/
|
||||
public beginFrame(): boolean {
|
||||
return this._atlas ? this._atlas.beginFrame() : true;
|
||||
if (!this._atlas) {
|
||||
return true;
|
||||
}
|
||||
if (this._atlas.pageLayoutVersion !== this._lastSeenPageLayoutVersion) {
|
||||
this._lastSeenPageLayoutVersion = this._atlas.pageLayoutVersion;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
|
||||
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
||||
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a one-off for
|
||||
// each glyph.
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
|
||||
}
|
||||
@@ -363,7 +376,13 @@ export class GlyphRenderer extends Disposable {
|
||||
// Bind the atlas page texture if they have changed. AtlasPage.version is globally
|
||||
// monotonic, so a page object swap at the same index (which happens after a page merge)
|
||||
// is detected by the same comparison.
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
// Clamp defensively in case the atlas unexpectedly exceeds texture capacity.
|
||||
const pageCount = Math.min(this._atlas.pages.length, this._atlasTextures.length);
|
||||
if (this._atlas.pages.length > this._atlasTextures.length && !this._pageOverflowWarned) {
|
||||
this._pageOverflowWarned = true;
|
||||
this._logService.warn(`Atlas page count (${this._atlas.pages.length}) exceeds the renderer's texture capacity (${this._atlasTextures.length}), some glyphs will not render correctly`);
|
||||
}
|
||||
for (let i = 0; i < pageCount; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
@@ -375,6 +394,7 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
this._lastSeenPageLayoutVersion = -1;
|
||||
this.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
@@ -389,8 +409,9 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
this._atlasTextures[i].version = atlas.pages[i].version;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'target-browser/renderer/shared/Types';
|
||||
import { IThemeService } from 'target-browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'target-browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { IThemeService } from 'browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IColor } from 'common/Types';
|
||||
@@ -13,7 +13,7 @@ import { Terminal } from '@xterm/xterm';
|
||||
import { RenderModelConstants } from './RenderModel';
|
||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { throwIfFalsy } from 'target-browser/renderer/shared/RendererUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { ICursorRenderModel, IRenderModel } from './Types';
|
||||
import { ISelectionRenderModel } from 'target-browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'target-browser/renderer/shared/SelectionRenderModel';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
|
||||
export const enum RenderModelConstants {
|
||||
INDICIES_PER_CELL = 4,
|
||||
|
||||