Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b1dbc2a71 | ||
|
|
be4b136e01 |
+27
-19
@@ -11,9 +11,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
./addons/addon-webgl/out/* \
|
||||
./addons/addon-webgl/out-*st/*
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: compressed-build.zip
|
||||
@@ -79,9 +79,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
@@ -100,16 +100,16 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -129,7 +129,6 @@ jobs:
|
||||
exit $EXIT_CODE
|
||||
|
||||
test-unit:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -137,16 +136,21 @@ jobs:
|
||||
runs-on: [ubuntu, macos, windows]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
npm ci
|
||||
- uses: actions/download-artifact@v7
|
||||
- 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
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -162,7 +166,6 @@ jobs:
|
||||
run: npm run test-unit --forbid-only
|
||||
|
||||
test-integration:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -171,9 +174,9 @@ jobs:
|
||||
browser: [chromium, firefox, webkit]
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
@@ -182,7 +185,12 @@ jobs:
|
||||
npm ci
|
||||
- name: Install playwright
|
||||
run: npx playwright install --with-deps ${{ matrix.browser }}
|
||||
- uses: actions/download-artifact@v7
|
||||
- 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
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
@@ -232,9 +240,9 @@ jobs:
|
||||
matrix:
|
||||
node-version: [22]
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'npm'
|
||||
@@ -243,7 +251,7 @@ jobs:
|
||||
npm ci
|
||||
- name: Install playwright
|
||||
run: npx playwright install
|
||||
- uses: actions/download-artifact@v7
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
|
||||
@@ -24,12 +24,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
uses: github/codeql-action/analyze@v2
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v5
|
||||
uses: actions/checkout@v4
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
|
||||
@@ -10,9 +10,9 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 22.x
|
||||
uses: actions/setup-node@v5
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22.x
|
||||
cache: 'npm'
|
||||
|
||||
@@ -17,7 +17,6 @@ docs/
|
||||
npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
mise.toml
|
||||
build/
|
||||
.DS_Store
|
||||
yarn.lock
|
||||
@@ -25,7 +24,6 @@ test-results/
|
||||
|
||||
# Tracing/debug files
|
||||
.cursor/nal-trace/
|
||||
.cursor/*.log
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
|
||||
-374
@@ -1,374 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -15,6 +15,9 @@
|
||||
"--recursive",
|
||||
"${workspaceRoot}/out/**/*.test.js"
|
||||
],
|
||||
"env": {
|
||||
"NODE_PATH": "${workspaceRoot}/out"
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
|
||||
@@ -26,7 +26,6 @@ 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
|
||||
@@ -99,15 +98,3 @@ const cell = line?.getCell(0);
|
||||
## Writing unit tests
|
||||
|
||||
- Unit tests live alongside the source code file of the thing it's testing with a .test.ts suffix.
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
**Demo server**: Start with `npm start` (port 3000). The demo server uses node-pty to spawn real shell sessions over WebSocket. Integration tests auto-start it via Playwright's `webServer` config, so you don't need to start it manually for `npm run test-integration`.
|
||||
|
||||
**Build before testing**: Always run `npm run build && npm run esbuild` before `npm run test-unit`. Integration tests also need `npm run esbuild-demo-client` and `npm run esbuild-demo-server`. The update script handles this automatically on session start.
|
||||
|
||||
**No external services**: This project has zero external dependencies (no databases, Docker, or APIs). Everything runs locally with Node.js.
|
||||
|
||||
**TypeScript compiler**: The project uses `tsgo` (native TypeScript compiler preview) rather than standard `tsc`. It's installed via the `@typescript/native-preview` package.
|
||||
|
||||
**Lint only changed files**: Prefer `npm run lint-changes` over `npm run lint` when iterating on code changes — it's significantly faster.
|
||||
|
||||
+1
-1
@@ -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 transferred 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 transfered 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,7 +254,6 @@ 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.
|
||||
- [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('AttachAddon', () => {
|
||||
test.describe('Search Tests', () => {
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ctx.proxy.reset();
|
||||
|
||||
@@ -78,43 +78,13 @@ 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 {
|
||||
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);
|
||||
return btoa(data);
|
||||
}
|
||||
public decodeText(data: string): string {
|
||||
if ((Uint8Array as IUint8ArrayB64Ctor).fromBase64 !== undefined) {
|
||||
try {
|
||||
return new TextDecoder().decode((Uint8Array as IUint8ArrayB64Ctor).fromBase64(data));
|
||||
} catch {}
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
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);
|
||||
return atob(data);
|
||||
} catch {}
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -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,22 +92,4 @@ 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`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ declare module '@xterm/addon-clipboard' {
|
||||
/**
|
||||
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
|
||||
*
|
||||
* Note that this clipboard provider does not implement own cut buffers as xterm does,
|
||||
* Note this 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{
|
||||
|
||||
@@ -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 = Math.max(0, parseInt(parentElementStyle.getPropertyValue('height'), 10) || 0);
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10) || 0);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
const elementStyle = _getComputedStyle(this._terminal.element);
|
||||
const elementPadding = {
|
||||
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
|
||||
top: parseInt(elementStyle.getPropertyValue('padding-top')),
|
||||
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
|
||||
right: parseInt(elementStyle.getPropertyValue('padding-right')),
|
||||
left: parseInt(elementStyle.getPropertyValue('padding-left'))
|
||||
};
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
## @xterm/addon-image
|
||||
|
||||
Inline image output in xterm.js. Supports SIXEL, iTerm's inline image protocol (IIP)
|
||||
and partially Kitty's terminal graphics prtocol (TGP).
|
||||
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).
|
||||
|
||||
|
||||

|
||||
@@ -44,6 +43,9 @@ terminal.loadAddon(imageAddon);
|
||||
|
||||
### General Notes
|
||||
|
||||
- *IMPORTANT:* The worker approach as done in previous releases got removed.
|
||||
The addon contructor no longer expects a worker path as first argument.
|
||||
|
||||
- By default the addon will activate these `windowOptions` reports on the terminal:
|
||||
- getWinSizePixels (CSI 14 t)
|
||||
- getCellSizePixels (CSI 16 t)
|
||||
@@ -71,7 +73,7 @@ terminal.loadAddon(imageAddon);
|
||||
|
||||
- **Cursor Positioning**
|
||||
If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode).
|
||||
Other cursor positioning modes as used by xterm or mintty are not supported for sixel.
|
||||
Other cursor positioning modes as used by xterm or mintty are not supported.
|
||||
|
||||
- **SIXEL Palette Handling**
|
||||
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
|
||||
@@ -90,7 +92,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 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.
|
||||
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.
|
||||
|
||||
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,24 +100,17 @@ 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 and GIF
|
||||
- No animation support.
|
||||
- Image type hinting is not supported (always deducted from data header).
|
||||
- File download is not supported.
|
||||
- Filename gets parsed but not used.
|
||||
- Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed).
|
||||
- Payload size may not exceed CEIL(sizeParameter * 4 / 3).
|
||||
- Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`).
|
||||
- Image pixel size is restricted by `pixelLimit` (pre- and post resizing).
|
||||
- Playload size is restricted by `iipSizeLimit`.
|
||||
- size parameter is not mandatory anymore.
|
||||
- Cursor gets positioned at the next cell of the last image line.
|
||||
- ReportCellSize sequence is supported.
|
||||
- Hi-res display support is currently limited (image gets blurred).
|
||||
|
||||
- **Kitty Graphics Support (TGP)**
|
||||
Set by default, change it with `{kittySupport: true}`.
|
||||
|
||||
Note that the kitty graphics support is still WIP.
|
||||
- Size parameter is restricted by `iipSizeLimit`.
|
||||
- Cursor positioning behaves the same as for sixel (see above).
|
||||
|
||||
|
||||
### Storage and Drawing Settings
|
||||
@@ -132,7 +127,6 @@ The addon exposes two properties to interact with the storage limits at runtime:
|
||||
By default the addon will show a placeholder pattern for evicted images that are still part
|
||||
of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`.
|
||||
|
||||
|
||||
### Image Data Retrieval
|
||||
|
||||
The addon provides the following API endpoints to retrieve raw image data as canvas:
|
||||
@@ -146,12 +140,11 @@ The addon provides the following API endpoints to retrieve raw image data as can
|
||||
The buffer position is the 0-based absolute index (including scrollback at top).
|
||||
Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions.
|
||||
|
||||
|
||||
### 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, 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.
|
||||
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.
|
||||
- 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 +170,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 bookkeeping 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 book keeping 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 only peaks temporarily, 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 is only temporarily peaking, 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 peak memory use during decoding.
|
||||
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
|
||||
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.
|
||||
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.
|
||||
- `sixelSizeLimit`
|
||||
A constructor setting. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken SIXEL sequences at an earlier phase, thus may stop the decoder from entering its memory intensive task for potentially invalid data.
|
||||
|
||||
|
||||
### Terminal Interaction
|
||||
@@ -218,5 +211,23 @@ _How can I adjust the memory usage?_
|
||||
|
||||
### Status
|
||||
|
||||
- Sixel and IIP support and image handling in xterm.js is considered beta quality.
|
||||
- Kitty support is in alpha stage. Please file a bug for any awkwardities or missing features.
|
||||
- Sixel support and image handling in xterm.js is considered beta quality.
|
||||
- IIP support is in alpha stage. Please file a bug for any awkwardities.
|
||||
|
||||
|
||||
### Changelog
|
||||
|
||||
- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
|
||||
- 0.4.3 defer canvas creation
|
||||
- 0.4.2 fix image canvas resize
|
||||
- 0.4.1 compat release for xterm.js 5.2.0
|
||||
- 0.4.0 IIP support
|
||||
- 0.3.1 compat release for xterm.js 5.1.0
|
||||
- 0.3.0 important change: worker removed from addon
|
||||
- 0.2.0 compat release for xterm.js 5.0.0
|
||||
- 0.1.3 bugfix: avoid striping
|
||||
- 0.1.2 bugfix: reset clear flag
|
||||
- 0.1.1 bugfixes:
|
||||
- clear sticky image tiles on render
|
||||
- create folder from bootstrap.sh
|
||||
- fix peer dependency in package.json
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ import { 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 { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
// Local const enum mirror - esbuild can't inline const enums from external packages
|
||||
@@ -23,7 +23,6 @@ const enum DecoderConst {
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
type: SequenceType.INVALID,
|
||||
name: 'Unnamed file',
|
||||
size: 0,
|
||||
width: 'auto',
|
||||
@@ -40,8 +39,6 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _dec: Base64Decoder;
|
||||
private _qoiDec: QoiDecoder;
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
private _isMultipart = false;
|
||||
private _abortMulti = false;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
@@ -55,14 +52,11 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._hp.reset();
|
||||
this._dec.release();
|
||||
this._qoiDec.release();
|
||||
}
|
||||
public reset(): void {}
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._header = DEFAULT_HEADER;
|
||||
this._metrics = UNSUPPORTED_TYPE;
|
||||
this._hp.reset();
|
||||
}
|
||||
@@ -82,27 +76,15 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
return;
|
||||
}
|
||||
if (dataPos > 0) {
|
||||
const seqType = this._hp.fields.type;
|
||||
if (seqType === SequenceType.FILE) {
|
||||
if (this._isMultipart) {
|
||||
this._isMultipart = false;
|
||||
this._abortMulti = false;
|
||||
this._dec.release();
|
||||
}
|
||||
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
||||
if (!this._header.inline) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init();
|
||||
} else if (this._abortMulti) {
|
||||
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
||||
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init();
|
||||
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
if (this._isMultipart) this._abortMulti = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,58 +93,22 @@ export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted) return true;
|
||||
|
||||
if (this._hp.state !== HeaderState.END) {
|
||||
if (this._hp.end()) return true;
|
||||
}
|
||||
const seqType = this._hp.fields.type;
|
||||
|
||||
if (seqType === SequenceType.FILEPART) return true;
|
||||
|
||||
if (seqType === SequenceType.REPORTCELLSIZE) {
|
||||
// OSC 1337 ; ReportCellSize=[height];[width];[scale] ST
|
||||
let width = CELL_SIZE_DEFAULT.width;
|
||||
let height = 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;
|
||||
}
|
||||
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
|
||||
const report = `\x1b]1337;ReportCellSize=${height.toFixed(3)};${width.toFixed(3)};${scale.toFixed(3)}\x1b\\`;
|
||||
this._coreTerminal.input(report, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (seqType === SequenceType.MULTIPARTFILE) {
|
||||
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
||||
this._isMultipart = true;
|
||||
this._abortMulti = false;
|
||||
this._dec.release();
|
||||
this._dec.init();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (seqType === SequenceType.FILEEND) {
|
||||
if (!this._isMultipart) return true;
|
||||
this._isMultipart = false;
|
||||
if (this._abortMulti || this._header.type !== SequenceType.MULTIPARTFILE) return true;
|
||||
}
|
||||
|
||||
// fallthrough for SequenceType.FILE & SequenceType.FILEEND
|
||||
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
|
||||
// early exit condition chain
|
||||
let cond: number | boolean;
|
||||
let cond: number | boolean = true;
|
||||
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;
|
||||
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;
|
||||
if (cond = this._dec.data8.length === this._header.size) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -222,8 +168,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), 10) * total / 100;
|
||||
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
|
||||
return parseInt(s, 10) * cdim;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,19 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { HeaderParser, HeaderState, IHeaderFields, SequenceType } from './IIPHeaderParser';
|
||||
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
|
||||
|
||||
|
||||
const CASES: [string, IHeaderFields][] = [
|
||||
['File=size=123456;name=dGVzdA==:', {type: SequenceType.FILE, name: 'test', size: 123456}],
|
||||
['File=size=123456;name=dGVzdA:', {type: SequenceType.FILE, name: 'test', size: 123456}],
|
||||
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
|
||||
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
|
||||
// utf-8 encoding in name
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw==:', {type: SequenceType.FILE, name: 'ümläutß', size: 123456}],
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw:', {type: SequenceType.FILE, name: 'ümläutß', size: 123456}],
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
|
||||
// full header spec
|
||||
[
|
||||
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
||||
{
|
||||
type: SequenceType.FILE,
|
||||
inline: 1,
|
||||
width: '10px',
|
||||
height: '20%',
|
||||
@@ -29,7 +28,6 @@ const CASES: [string, IHeaderFields][] = [
|
||||
[
|
||||
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
||||
{
|
||||
type: SequenceType.FILE,
|
||||
inline: 1,
|
||||
width: 'auto',
|
||||
height: '20',
|
||||
@@ -103,7 +101,7 @@ describe('IIPHeaderParser', () => {
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: 'test', size: 0});
|
||||
assert.deepEqual(hp.fields, {name: 'test', size: 0});
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
@@ -117,7 +115,7 @@ describe('IIPHeaderParser', () => {
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: '', size: 123456});
|
||||
assert.deepEqual(hp.fields, {name: '', size: 123456});
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
@@ -137,124 +135,4 @@ describe('IIPHeaderParser', () => {
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
it('FilePart sequence', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('FilePart=w7xtbMOkdXTDnw');
|
||||
const res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, 9);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields.type, SequenceType.FILEPART);
|
||||
});
|
||||
it('FilePart sequence - bytewise', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('FilePart=w7xtbMOkdXTDnw');
|
||||
let res = -2;
|
||||
let pos = 0;
|
||||
while (res === -2 && pos < inp.length) {
|
||||
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
||||
}
|
||||
assert.strictEqual(res, 1);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields.type, SequenceType.FILEPART);
|
||||
});
|
||||
it('MultipartFile sequence', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('MultipartFile=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.VALUE);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(
|
||||
hp.fields,
|
||||
{
|
||||
type: SequenceType.MULTIPARTFILE,
|
||||
inline: 1,
|
||||
width: '10px',
|
||||
height: '20%',
|
||||
preserveAspectRatio: 1,
|
||||
size: 123456,
|
||||
name: 'ümläutß'
|
||||
}
|
||||
);
|
||||
});
|
||||
it('MultipartFile sequence - bytewise', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('MultipartFile=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw');
|
||||
let res = -2;
|
||||
let pos = 0;
|
||||
while (res === -2 && pos < inp.length) {
|
||||
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
||||
}
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.VALUE);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(
|
||||
hp.fields,
|
||||
{
|
||||
type: SequenceType.MULTIPARTFILE,
|
||||
inline: 1,
|
||||
width: '10px',
|
||||
height: '20%',
|
||||
preserveAspectRatio: 1,
|
||||
size: 123456,
|
||||
name: 'ümläutß'
|
||||
}
|
||||
);
|
||||
});
|
||||
it('FileEnd sequence', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('FileEnd');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.START);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields.type, SequenceType.FILEEND);
|
||||
});
|
||||
it('FileEnd sequence - bytewise', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('FileEnd');
|
||||
let res = -2;
|
||||
let pos = 0;
|
||||
while (res === -2 && pos < inp.length) {
|
||||
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
||||
}
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.START);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields.type, SequenceType.FILEEND);
|
||||
});
|
||||
it('ReportCellSize sequence', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('ReportCellSize');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.START);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, { type: SequenceType.REPORTCELLSIZE });
|
||||
});
|
||||
it('ReportCellSize sequence - bytewise', () => {
|
||||
const hp = new HeaderParser();
|
||||
const inp = fromBs('ReportCellSize');
|
||||
let res = -2;
|
||||
let pos = 0;
|
||||
while (res === -2 && pos < inp.length) {
|
||||
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
||||
}
|
||||
assert.strictEqual(res, -2);
|
||||
assert.strictEqual(hp.state, HeaderState.START);
|
||||
res = hp.end();
|
||||
assert.strictEqual(res, 0);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, { type: SequenceType.REPORTCELLSIZE });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,27 +6,9 @@
|
||||
// eslint-disable-next-line
|
||||
declare const Buffer: any;
|
||||
|
||||
export const enum HeaderState {
|
||||
START = 0,
|
||||
ABORT = 1,
|
||||
KEY = 2,
|
||||
VALUE = 3,
|
||||
END = 4
|
||||
}
|
||||
|
||||
export const enum SequenceType {
|
||||
INVALID = 0,
|
||||
FILE = 1,
|
||||
MULTIPARTFILE = 2,
|
||||
FILEPART = 3,
|
||||
FILEEND = 4,
|
||||
REPORTCELLSIZE = 5
|
||||
}
|
||||
|
||||
export interface IHeaderFields {
|
||||
[key: string]: number | string | Uint32Array | null | undefined;
|
||||
// sequence type
|
||||
type: SequenceType;
|
||||
// base-64 encoded filename. Defaults to "Unnamed file".
|
||||
name: string;
|
||||
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
||||
@@ -47,6 +29,14 @@ export interface IHeaderFields {
|
||||
inline?: number;
|
||||
}
|
||||
|
||||
export const enum HeaderState {
|
||||
START = 0,
|
||||
ABORT = 1,
|
||||
KEY = 2,
|
||||
VALUE = 3,
|
||||
END = 4
|
||||
}
|
||||
|
||||
// field value decoders
|
||||
|
||||
// ASCII bytes to string
|
||||
@@ -102,19 +92,7 @@ const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
|
||||
};
|
||||
|
||||
|
||||
// sequence type markers
|
||||
// File
|
||||
const FILE_MARKER = [70, 105, 108, 101];
|
||||
// MultipartFile
|
||||
const MULTIPARTFILE_MARKER = [77, 117, 108, 116, 105, 112, 97, 114, 116, 70, 105, 108, 101];
|
||||
// FilePart
|
||||
const FILEPART_MARKER = [70, 105, 108, 101, 80, 97, 114, 116];
|
||||
// FileEnd
|
||||
const FILEEND_MARKER = [70, 105, 108, 101, 69, 110, 100];
|
||||
// ReportCellSize
|
||||
const REPORTCELLSIZE_MARKER = [82, 101, 112, 111, 114, 116, 67, 101, 108, 108, 83, 105, 122, 101];
|
||||
|
||||
// max allowed chars for sequence header
|
||||
const MAX_FIELDCHARS = 1024;
|
||||
|
||||
|
||||
@@ -133,43 +111,12 @@ export class HeaderParser {
|
||||
this._key = '';
|
||||
}
|
||||
|
||||
public end(): number {
|
||||
if (this.state === HeaderState.START) {
|
||||
if (this._position === FILEEND_MARKER.length) {
|
||||
for (let k = 0; k < FILEEND_MARKER.length; ++k) {
|
||||
if (this._buffer[k] !== FILEEND_MARKER[k]) return this._a();
|
||||
}
|
||||
this.fields['type'] = SequenceType.FILEEND;
|
||||
this.state = HeaderState.END;
|
||||
return 0;
|
||||
}
|
||||
if (this._position === REPORTCELLSIZE_MARKER.length) {
|
||||
for (let k = 0; k < REPORTCELLSIZE_MARKER.length; ++k) {
|
||||
if (this._buffer[k] !== REPORTCELLSIZE_MARKER[k]) return this._a();
|
||||
}
|
||||
this.fields['type'] = SequenceType.REPORTCELLSIZE;
|
||||
this.state = HeaderState.END;
|
||||
return 0;
|
||||
}
|
||||
return this._a();
|
||||
}
|
||||
if (this.state === HeaderState.END) return 0;
|
||||
if (this.state === HeaderState.VALUE
|
||||
&& this.fields.type === SequenceType.MULTIPARTFILE
|
||||
) {
|
||||
if (!this._storeValue(this._position)) return this._a();
|
||||
this.state = HeaderState.END;
|
||||
return 0;
|
||||
}
|
||||
return this._a();
|
||||
}
|
||||
|
||||
public parse(data: Uint32Array, start: number, end: number): number {
|
||||
let state = this.state;
|
||||
let pos = this._position;
|
||||
const buffer = this._buffer;
|
||||
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
|
||||
if (state === HeaderState.START && pos > 14) return -1;
|
||||
if (state === HeaderState.START && pos > 6) return -1;
|
||||
for (let i = start; i < end; ++i) {
|
||||
const c = data[i];
|
||||
switch (c) {
|
||||
@@ -180,29 +127,8 @@ export class HeaderParser {
|
||||
break;
|
||||
case 61: // =
|
||||
if (state === HeaderState.START) {
|
||||
if (buffer[0] === 70) {
|
||||
// 'File' or 'FilePart'
|
||||
let k = 0;
|
||||
for (; k < FILE_MARKER.length; ++k) {
|
||||
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
||||
}
|
||||
this.fields['type'] = SequenceType.FILE;
|
||||
if (pos === FILEPART_MARKER.length) {
|
||||
for (; k < FILEPART_MARKER.length; ++k) {
|
||||
if (buffer[k] !== FILEPART_MARKER[k]) return this._a();
|
||||
}
|
||||
this.fields['type'] = SequenceType.FILEPART;
|
||||
this.state = HeaderState.END;
|
||||
return i + 1;
|
||||
}
|
||||
} else if (buffer[0] === 77) {
|
||||
// 'MultipartFile'
|
||||
for (let k = 0; k < MULTIPARTFILE_MARKER.length; ++k) {
|
||||
if (buffer[k] !== MULTIPARTFILE_MARKER[k]) return this._a();
|
||||
}
|
||||
this.fields['type'] = SequenceType.MULTIPARTFILE;
|
||||
} else {
|
||||
return this._a();
|
||||
for (let k = 0; k < FILE_MARKER.length; ++k) {
|
||||
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
||||
}
|
||||
state = HeaderState.KEY;
|
||||
pos = 0;
|
||||
@@ -232,7 +158,6 @@ export class HeaderParser {
|
||||
}
|
||||
|
||||
private _a(): number {
|
||||
this.fields.type = SequenceType.INVALID;
|
||||
this.state = HeaderState.ABORT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IAddImageOpts } from './Types';
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
|
||||
/**
|
||||
@@ -13,7 +12,6 @@ import { ImageStorage } from './ImageStorage';
|
||||
* - Always uses scrolling mode (cursor advances with image)
|
||||
*/
|
||||
export class IIPImageStorage {
|
||||
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
) {}
|
||||
@@ -23,6 +21,6 @@ export class IIPImageStorage {
|
||||
* Always uses scrolling mode — cursor advances past the image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._storage.addImage(img, this._addImageOpts);
|
||||
this._storage.addImage(img, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,7 @@ const TEST_IMAGES: [string, IMetrics][] = [
|
||||
['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 }],
|
||||
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }]
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -137,6 +137,11 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
|
||||
// enable size reports
|
||||
if (this._opts.enableSizeReports) {
|
||||
// const windowOptions = terminal.getOption('windowOptions');
|
||||
// windowOptions.getWinSizePixels = true;
|
||||
// windowOptions.getCellSizePixels = true;
|
||||
// windowOptions.getWinSizeChars = true;
|
||||
// terminal.setOption('windowOptions', windowOptions);
|
||||
const windowOps = terminal.options.windowOptions ?? {};
|
||||
windowOps.getWinSizePixels = true;
|
||||
windowOps.getCellSizePixels = true;
|
||||
@@ -255,7 +260,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
|
||||
}
|
||||
|
||||
private _report(s: string): void {
|
||||
this._terminal?._core.input(s, false);
|
||||
this._terminal?._core.coreService.triggerDataEvent(s);
|
||||
}
|
||||
|
||||
private _decset(params: (number | number[])[]): boolean {
|
||||
|
||||
@@ -146,7 +146,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
|
||||
public clearLines(start: number, end: number, layer?: ImageLayer): void {
|
||||
const y = start * (this.dimensions?.css.cell.height || 0);
|
||||
const w = this.dimensions?.css.canvas.width || 0;
|
||||
const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
|
||||
const h = (++end - start) * (this.dimensions?.css.cell.height || 0);
|
||||
if (!layer || layer === 'top') {
|
||||
this._layers.get('top')?.clearRect(0, y, w, h);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import {
|
||||
ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
|
||||
IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
|
||||
UnderlineStyle, IAddImageOpts
|
||||
} from './Types';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
|
||||
|
||||
|
||||
// fallback default cell size
|
||||
@@ -239,15 +235,13 @@ export class ImageStorage implements IDisposable {
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
* @param img - The image to add (canvas or bitmap).
|
||||
* @param opts - Options for addImage:
|
||||
* - scrolling: When true, cursor advances with the image.
|
||||
* When false, image is placed at ORIGIN and cursor does not move.
|
||||
* - layer: Which canvas layer to render on ('top' or 'bottom').
|
||||
* - zIndex: Z-index for image layering within the same layer.
|
||||
* - cursorPos: 'vt340' for bottom-left, 'iip' for bottom.right.
|
||||
* @param scrolling - When true, cursor advances with the image (lineFeed per row).
|
||||
* When false, image is placed at (0,0) and cursor is restored (DECSET 80 / sixel origin mode).
|
||||
* @param layer - Which canvas layer to render on ('top' or 'bottom').
|
||||
* @param zIndex - Z-index for image layering within the same layer.
|
||||
* @returns The internal image ID assigned to the stored image.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
@@ -269,7 +263,7 @@ export class ImageStorage implements IDisposable {
|
||||
let offset = originX;
|
||||
let tileCount = 0;
|
||||
|
||||
if (!opts.scrolling) {
|
||||
if (!scrolling) {
|
||||
buffer.x = 0;
|
||||
buffer.y = 0;
|
||||
offset = 0;
|
||||
@@ -283,7 +277,7 @@ export class ImageStorage implements IDisposable {
|
||||
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
||||
tileCount++;
|
||||
}
|
||||
if (opts.scrolling) {
|
||||
if (scrolling) {
|
||||
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
||||
} else {
|
||||
if (++buffer.y >= termRows) break;
|
||||
@@ -293,12 +287,8 @@ export class ImageStorage implements IDisposable {
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
|
||||
// cursor positioning modes
|
||||
if (opts.scrolling) {
|
||||
if (opts.cursorPos === 'iip') {
|
||||
buffer.x = Math.min(offset + cols, termCols);
|
||||
} else {
|
||||
buffer.x = offset;
|
||||
}
|
||||
if (scrolling) {
|
||||
buffer.x = offset;
|
||||
} else {
|
||||
buffer.x = originX;
|
||||
buffer.y = originY;
|
||||
@@ -341,8 +331,8 @@ export class ImageStorage implements IDisposable {
|
||||
marker: endMarker || undefined,
|
||||
tileCount,
|
||||
bufferType: this._terminal.buffer.active.type,
|
||||
layer: opts.layer,
|
||||
zIndex: opts.zIndex
|
||||
layer,
|
||||
zIndex
|
||||
};
|
||||
|
||||
// finally add the image
|
||||
@@ -465,6 +455,7 @@ export class ImageStorage implements IDisposable {
|
||||
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
|
||||
break;
|
||||
}
|
||||
e = nextE;
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
|
||||
import { IImageAddonOptions, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,6 @@ import { ImageRenderer } from './ImageRenderer';
|
||||
* - advanceCursor for empty sixels carrying only height
|
||||
*/
|
||||
export class SixelImageStorage {
|
||||
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'vt340' };
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
@@ -28,8 +27,7 @@ export class SixelImageStorage {
|
||||
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
this._addImageOpts.scrolling = this._opts.sixelScrolling;
|
||||
this._storage.addImage(img, this._addImageOpts);
|
||||
this._storage.addImage(img, this._opts.sixelScrolling);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,8 +9,7 @@ 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 { IInputHandler } from 'common/Types';
|
||||
import type { IBufferLine, IExtendedAttrs } from 'common/buffer/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, 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';
|
||||
@@ -101,14 +100,6 @@ export interface ICellSize {
|
||||
}
|
||||
|
||||
export type ImageLayer = 'top' | 'bottom';
|
||||
export type CursorPos = 'vt340' | 'iip';
|
||||
|
||||
export interface IAddImageOpts {
|
||||
scrolling: boolean;
|
||||
layer: ImageLayer;
|
||||
zIndex: number;
|
||||
cursorPos: CursorPos;
|
||||
}
|
||||
|
||||
export interface IImageSpec {
|
||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
||||
|
||||
@@ -168,7 +168,7 @@ export function parseKittyCommand(data: string): IKittyCommand {
|
||||
cmd.deleteSelector = value;
|
||||
continue;
|
||||
}
|
||||
const numValue = parseInt(value, 10);
|
||||
const numValue = parseInt(value);
|
||||
switch (key) {
|
||||
case KittyKey.FORMAT: cmd.format = numValue; break;
|
||||
case KittyKey.ID: cmd.id = numValue; break;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IDisposable } from '@xterm/xterm';
|
||||
import { ImageStorage } from '../ImageStorage';
|
||||
import { ImageLayer, IAddImageOpts } from '../Types';
|
||||
import { ImageLayer } from '../Types';
|
||||
import { IKittyImageData } from './KittyGraphicsTypes';
|
||||
|
||||
// Kitty-specific image storage controller.
|
||||
@@ -42,7 +42,6 @@ export class KittyImageStorage implements IDisposable {
|
||||
this._images.delete(kittyId);
|
||||
}
|
||||
};
|
||||
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
|
||||
|
||||
constructor(
|
||||
private readonly _storage: ImageStorage
|
||||
@@ -99,10 +98,7 @@ export class KittyImageStorage implements IDisposable {
|
||||
if (oldStorageId !== undefined) {
|
||||
this._storageIdToKittyId.delete(oldStorageId);
|
||||
}
|
||||
this._addImageOpts.scrolling = scrolling;
|
||||
this._addImageOpts.layer = layer;
|
||||
this._addImageOpts.zIndex = zIndex;
|
||||
const storageId = this._storage.addImage(image, this._addImageOpts);
|
||||
const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
|
||||
this._kittyIdToStorageId.set(kittyId, storageId);
|
||||
this._storageIdToKittyId.set(storageId, kittyId);
|
||||
}
|
||||
|
||||
@@ -290,67 +290,6 @@ test.describe('ImageAddon', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('IIP support', () => {
|
||||
test('size params is optional/informative', async () => {
|
||||
// too small size param
|
||||
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=5:${PALETTE_PNG_BASE64}\x07`);
|
||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
||||
// no size aparm at all
|
||||
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1:${PALETTE_QOI_BASE64}\x07`);
|
||||
deepStrictEqual(await getOrigSize(2), [640, 80]);
|
||||
});
|
||||
test.skip('FilePart support - bytewise', async () => {
|
||||
// opener
|
||||
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
|
||||
// byte-wise
|
||||
for (let i = 0; i < PALETTE_PNG_BASE64.length; ++i) {
|
||||
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64[i]}\x07`);
|
||||
}
|
||||
// finalizer
|
||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
||||
});
|
||||
test('FilePart support - chunks', async () => {
|
||||
// opener
|
||||
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
|
||||
// chunks
|
||||
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
|
||||
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
|
||||
}
|
||||
// finalizer
|
||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||
deepStrictEqual(await getOrigSize(1), [640, 80]);
|
||||
});
|
||||
test('FilePart support - edgecases', async () => {
|
||||
// multiple opener do not harm
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
|
||||
}
|
||||
// multiple finalizer do not harm
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||
}
|
||||
// chunks w'o opener are ignored
|
||||
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
|
||||
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
|
||||
}
|
||||
await ctx.proxy.write(`\x1b]1337;FileEnd\x07`);
|
||||
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 0);
|
||||
});
|
||||
test('FilePart support - other IIP sequence cancels chunked image', async () => {
|
||||
// opener
|
||||
await ctx.proxy.write(`\x1b]1337;MultipartFile=inline=1\x07`);
|
||||
// chunks
|
||||
for (let i = 0; i < PALETTE_PNG_BASE64.length; i += 256) {
|
||||
await ctx.proxy.write(`\x1b]1337;FilePart=${PALETTE_PNG_BASE64.slice(i, i + 256)}\x07`);
|
||||
}
|
||||
// finalizer skipped
|
||||
// write spinfox.png
|
||||
await ctx.proxy.write(TESTDATA_IIP[1][0]);
|
||||
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('IIP support - testimages', () => {
|
||||
test('palette.png', async () => {
|
||||
await ctx.proxy.write(TESTDATA_IIP[0][0]);
|
||||
|
||||
@@ -539,6 +539,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
|
||||
test.describe('Query support (a=q)', () => {
|
||||
test('responds with OK for capability query without payload', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -547,11 +548,12 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=31;OK\x1b\\');
|
||||
});
|
||||
|
||||
test('responds with OK for valid PNG query', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -560,7 +562,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write(`\x1b_Gi=42,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=42;OK\x1b\\');
|
||||
});
|
||||
|
||||
@@ -576,6 +578,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
});
|
||||
|
||||
test('responds with error for invalid base64', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -584,11 +587,12 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response: string = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response.startsWith('\x1b_Gi=60;EINVAL:'), true);
|
||||
});
|
||||
|
||||
test('responds with error for RGB data without dimensions', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -597,7 +601,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=70;EINVAL:width and height required for raw pixel data\x1b\\');
|
||||
});
|
||||
|
||||
@@ -638,6 +642,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
});
|
||||
|
||||
test('responds with EINVAL when both i and I keys are specified', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -647,11 +652,12 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write(`\x1b_Gi=100,I=200,a=q,f=100;${KITTY_BLACK_1X1_BASE64}\x1b\\`);
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=100;EINVAL:cannot specify both i and I keys\x1b\\');
|
||||
});
|
||||
|
||||
test('responds with EINVAL for i+I conflict even without payload', async () => {
|
||||
let response = '';
|
||||
await ctx.page.evaluate(() => {
|
||||
(window as any).kittyResponse = '';
|
||||
(window as any).term.onData((data: string) => { (window as any).kittyResponse = data; });
|
||||
@@ -661,7 +667,7 @@ test.describe('Kitty Graphics Protocol', () => {
|
||||
await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\');
|
||||
await timeout(100);
|
||||
|
||||
const response = await ctx.page.evaluate('window.kittyResponse');
|
||||
response = await ctx.page.evaluate('window.kittyResponse');
|
||||
strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,11 +31,15 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.3.6",
|
||||
"opentype.js": "^2.0.0"
|
||||
"lru-cache": "^6.0.0",
|
||||
"opentype.js": "^0.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/opentype.js": "^1.3.9",
|
||||
"font-finder": "^1.1.0"
|
||||
"@types/lru-cache": "^5.1.0",
|
||||
"@types/opentype.js": "^0.7.0",
|
||||
"axios": "^1.15.2",
|
||||
"font-finder": "^1.1.0",
|
||||
"mkdirp": "0.5.5",
|
||||
"yauzl": "^3.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as opentype from 'opentype.js';
|
||||
import { LRUCache } from 'lru-cache';
|
||||
import LRUCache = require('lru-cache');
|
||||
|
||||
import { IFont, ILigatureData, IFlattenedLookupTree, ILookupTree, IOptions } from './types';
|
||||
import mergeTrees from './merge';
|
||||
@@ -23,8 +23,8 @@ class FontImpl implements IFont {
|
||||
|
||||
if (options.cacheSize > 0) {
|
||||
this._cache = new LRUCache({
|
||||
maxSize: options.cacheSize,
|
||||
sizeCalculation: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
|
||||
max: options.cacheSize,
|
||||
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ export default function mergeRange(ranges: [number, number][], newRangeStart: nu
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRangeEnd, range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
// Case 7: New range extends from previous range past the
|
||||
|
||||
@@ -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 terminal. If
|
||||
* properly, this must be called after `open()` is called on the therminal. If
|
||||
* the font currently in use supports ligatures, the terminal will automatically
|
||||
* start to render them.
|
||||
* @param term Terminal instance from xterm.js
|
||||
|
||||
@@ -9,18 +9,15 @@ import { ITestContext, createTestContext, openTerminal } from '../../../test/pla
|
||||
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
ctx.page.setViewportSize({ width: 1024, height: 768 });
|
||||
await openTerminal(ctx);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
|
||||
test.describe('ProgressAddon', () => {
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
await openTerminal(ctx);
|
||||
});
|
||||
test.afterAll(async () => {
|
||||
await ctx?.page?.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async function(): Promise<any> {
|
||||
await ctx.page.evaluate(`
|
||||
window.progressStack = [];
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
/**
|
||||
* 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,14 +142,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
|
||||
}
|
||||
prevResult = result;
|
||||
results.push(prevResult);
|
||||
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);
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
this._resultTracker.updateResults(results, this._highlightLimit);
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ export class SearchEngine {
|
||||
this._terminal.clearSelection();
|
||||
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
|
||||
const startCol = this._terminal.cols;
|
||||
let startCol = this._terminal.cols;
|
||||
const isReverseSearch = true;
|
||||
|
||||
this._lineCache.initLinesCache();
|
||||
@@ -186,14 +186,14 @@ export class SearchEngine {
|
||||
let result: ISearchResult | undefined;
|
||||
if (prevSelectedPos) {
|
||||
searchPosition.startRow = startRow = prevSelectedPos.start.y;
|
||||
searchPosition.startCol = prevSelectedPos.start.x;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.start.x;
|
||||
if (cachedSearchTerm !== term) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
if (!result) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
searchPosition.startRow = startRow = prevSelectedPos.end.y;
|
||||
searchPosition.startCol = prevSelectedPos.end.x;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.end.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,24 +414,10 @@ 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>((res, rej) => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => err ? rej(err) : res(data)))).toString();
|
||||
fixture = (await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)))).toString();
|
||||
if (process.platform !== 'win32') {
|
||||
fixture = fixture.replace(/\n/g, '\n\r');
|
||||
}
|
||||
@@ -515,7 +501,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 }
|
||||
|
||||
@@ -7,10 +7,10 @@
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2021",
|
||||
"module": "nodenext",
|
||||
"module": "commonjs",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start",
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IAttributeData } from 'common/buffer/Types';
|
||||
import { IAttributeData, IColor } from 'common/Types';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/Types';
|
||||
import { UnderlineStyle } from 'common/buffer/Constants';
|
||||
|
||||
@@ -173,7 +172,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, we must pad it back, or the color block
|
||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
|
||||
// will missing
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// use clear right to set background.
|
||||
@@ -221,7 +220,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 simplify the process to determine line separator
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
@@ -233,7 +232,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 simplify the process to determine line separator
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
|
||||
@@ -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 at the end, serialize will use \x1b[0m to clear instead of the specific disable sequence
|
||||
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(INVERSE) + line,
|
||||
sgr(BOLD) + line,
|
||||
sgr(UNDERLINED) + line,
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"rootDir": "..",
|
||||
"outDir": "../out-benchmark",
|
||||
"types": ["../../../node_modules/@types/node"],
|
||||
"moduleResolution": "nodenext",
|
||||
"moduleResolution": "node",
|
||||
"strict": false,
|
||||
"target": "es2015",
|
||||
"module": "nodenext",
|
||||
"module": "commonjs",
|
||||
"paths": {
|
||||
"common/*": ["../../../src/common/*"],
|
||||
"browser/*": ["../../../src/browser/*"],
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"start": "node ../../demo/start",
|
||||
"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"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* UnicodeVersionProvider for V15 with grapheme cluster handling.
|
||||
* UnicodeVersionProvider for V15 with grapeme cluster handleing.
|
||||
*/
|
||||
|
||||
import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||
|
||||
@@ -78,22 +78,7 @@ test.describe('WebFontsAddon', () => {
|
||||
const data = await ctx.page.evaluate(`
|
||||
document.styleSheets[0].insertRule("@font-face {font-family: Kongtext; src: url(/fonts/kongtext.regular.ttf) format('truetype'); unicode-range: U+00A0-00FF}", 0);
|
||||
`);
|
||||
let fonts = await getDocumentFonts();
|
||||
// safari may have loaded the font, firefox & chrome never load it
|
||||
if (browser.browserType().name() === 'webkit') {
|
||||
try {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loading' }]);
|
||||
} catch (e) {
|
||||
try {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
} catch (e) {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
}
|
||||
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
|
||||
// broken case: webfont in ctor without addon usage
|
||||
await ctx.page.evaluate(`
|
||||
@@ -101,20 +86,15 @@ test.describe('WebFontsAddon', () => {
|
||||
window.helperTerm.open(term.element);
|
||||
`);
|
||||
|
||||
fonts = await getDocumentFonts();
|
||||
// safari might load the font, firefox & chrome dont
|
||||
// safari loads the font, firefox & chrome dont
|
||||
if (browser.browserType().name() === 'webkit') {
|
||||
try {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loading' }]);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loading' }]);
|
||||
} catch (e) {
|
||||
try {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
} catch (e) {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
}
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
|
||||
}
|
||||
} else {
|
||||
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
|
||||
}
|
||||
|
||||
// good case: addon fixes layout for webfont in ctor
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"lib": [
|
||||
"es2021",
|
||||
],
|
||||
// "downlevelIteration": true,
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
|
||||
@@ -57,8 +57,7 @@ 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 flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
||||
const rex = new RegExp(regex.source, flags);
|
||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
||||
|
||||
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
||||
const line = lines.join('');
|
||||
@@ -114,8 +113,8 @@ export class LinkComputer {
|
||||
let line: IBufferLine | undefined;
|
||||
let topIdx = lineIndex;
|
||||
let bottomIdx = lineIndex;
|
||||
let length;
|
||||
let content;
|
||||
let length = 0;
|
||||
let content = '';
|
||||
const lines: string[] = [];
|
||||
|
||||
if ((line = terminal.buffer.active.getLine(lineIndex))) {
|
||||
|
||||
@@ -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 everything starting with http:// or https://
|
||||
// consider everthing 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 [] for ipv6 addresses, also allow ~)
|
||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
|
||||
// also exclude as finals:
|
||||
// - final interpunction like ,.!?
|
||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"lib": [
|
||||
"es2021",
|
||||
],
|
||||
// "downlevelIteration": true,
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
|
||||
@@ -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 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 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:
|
||||
|
||||
```ts
|
||||
const terminal = new Terminal();
|
||||
|
||||
@@ -3,7 +3,7 @@ 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/buffer/Types';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { rgba } from 'common/Color';
|
||||
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { configEquals } from './CharAtlasUtils';
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
import { IColor } from 'common/Types';
|
||||
|
||||
function createTestConfig(overrides: Partial<ICharAtlasConfig> = {}): ICharAtlasConfig {
|
||||
const color: IColor = { css: '#ffffff', rgba: 0xffffffff };
|
||||
const contrastCache = {
|
||||
clear: () => {},
|
||||
setCss: () => {},
|
||||
getCss: () => undefined,
|
||||
setColor: () => {},
|
||||
getColor: () => undefined
|
||||
};
|
||||
const colors = {
|
||||
foreground: color,
|
||||
background: color,
|
||||
cursor: NULL_COLOR,
|
||||
cursorAccent: NULL_COLOR,
|
||||
selectionForeground: undefined,
|
||||
selectionBackgroundTransparent: NULL_COLOR,
|
||||
selectionBackgroundOpaque: NULL_COLOR,
|
||||
selectionInactiveBackgroundTransparent: NULL_COLOR,
|
||||
selectionInactiveBackgroundOpaque: NULL_COLOR,
|
||||
overviewRulerBorder: NULL_COLOR,
|
||||
scrollbarSliderBackground: NULL_COLOR,
|
||||
scrollbarSliderHoverBackground: NULL_COLOR,
|
||||
scrollbarSliderActiveBackground: NULL_COLOR,
|
||||
ansi: new Array(256).fill(color),
|
||||
contrastCache,
|
||||
halfContrastCache: contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: true,
|
||||
devicePixelRatio: 1,
|
||||
deviceMaxTextureSize: 4096,
|
||||
letterSpacing: 0,
|
||||
lineHeight: 1,
|
||||
fontSize: 15,
|
||||
fontFamily: 'monospace',
|
||||
fontWeight: 'normal',
|
||||
fontWeightBold: 'bold',
|
||||
deviceCellWidth: 10,
|
||||
deviceCellHeight: 20,
|
||||
deviceCharWidth: 8,
|
||||
deviceCharHeight: 16,
|
||||
allowTransparency: false,
|
||||
drawBoldTextInBrightColors: true,
|
||||
minimumContrastRatio: 1,
|
||||
colors,
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
|
||||
describe('CharAtlasUtils', () => {
|
||||
describe('configEquals', () => {
|
||||
it('should return true for identical configs', () => {
|
||||
const a = createTestConfig();
|
||||
const b = createTestConfig();
|
||||
assert.ok(configEquals(a, b));
|
||||
});
|
||||
|
||||
it('should return false when deviceMaxTextureSize differs', () => {
|
||||
const a = createTestConfig();
|
||||
const b = createTestConfig({ deviceMaxTextureSize: 8192 });
|
||||
assert.ok(!configEquals(a, b));
|
||||
});
|
||||
|
||||
it('should return false when deviceCellWidth differs', () => {
|
||||
const a = createTestConfig();
|
||||
const b = createTestConfig({ deviceCellWidth: 11 });
|
||||
assert.ok(!configEquals(a, b));
|
||||
});
|
||||
|
||||
it('should return false when deviceCellHeight differs', () => {
|
||||
const a = createTestConfig();
|
||||
const b = createTestConfig({ deviceCellHeight: 21 });
|
||||
assert.ok(!configEquals(a, b));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -59,7 +59,6 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.deviceMaxTextureSize === b.deviceMaxTextureSize &&
|
||||
a.customGlyphs === b.customGlyphs &&
|
||||
a.lineHeight === b.lineHeight &&
|
||||
a.letterSpacing === b.letterSpacing &&
|
||||
@@ -68,8 +67,6 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.deviceCellWidth === b.deviceCellWidth &&
|
||||
a.deviceCellHeight === b.deviceCellHeight &&
|
||||
a.deviceCharWidth === b.deviceCharWidth &&
|
||||
a.deviceCharHeight === b.deviceCharHeight &&
|
||||
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
|
||||
|
||||
@@ -9,7 +9,7 @@ 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 type { IOptionsService } from 'common/services/Services';
|
||||
import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
interface IVertices {
|
||||
@@ -100,8 +100,6 @@ export class GlyphRenderer extends Disposable {
|
||||
private readonly _attributesBuffer: WebGLBuffer;
|
||||
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
private _lastAtlasPagesVersion: number = -1;
|
||||
private _pageOverflowWarned: boolean = false;
|
||||
private _activeBuffer: number = 0;
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
@@ -116,8 +114,7 @@ export class GlyphRenderer extends Disposable {
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _logService: ILogService
|
||||
private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -130,7 +127,7 @@ export class GlyphRenderer extends Disposable {
|
||||
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
|
||||
}
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages), this._logService));
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
|
||||
this._register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
@@ -139,9 +136,8 @@ export class GlyphRenderer extends Disposable {
|
||||
this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
const vertexArrayObject = this._vertexArrayObject = gl.createVertexArray();
|
||||
this._register(toDisposable(() => gl.deleteVertexArray(vertexArrayObject)));
|
||||
gl.bindVertexArray(vertexArrayObject);
|
||||
this._vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
@@ -215,27 +211,14 @@ export class GlyphRenderer extends Disposable {
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the full model must be rebuilt before rendering this frame.
|
||||
* This is tracked per renderer (not on the shared atlas) so that every
|
||||
* terminal sharing the atlas rebuilds its model after a page merge, not just
|
||||
* the first one to render (vscode#322756).
|
||||
*/
|
||||
public beginFrame(): boolean {
|
||||
if (!this._atlas) {
|
||||
return true;
|
||||
}
|
||||
if (this._atlas.pagesVersion !== this._lastAtlasPagesVersion) {
|
||||
this._lastAtlasPagesVersion = this._atlas.pagesVersion;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this._atlas ? this._atlas.beginFrame() : true;
|
||||
}
|
||||
|
||||
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 one-off for
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
||||
// each glyph.
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
|
||||
}
|
||||
@@ -377,15 +360,8 @@ 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. The page count is clamped to the fixed texture
|
||||
// array size so an atlas page overflow degrades (untextured glyphs) instead of throwing
|
||||
// mid-frame; the atlas is responsible for never exceeding maxAtlasPages.
|
||||
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++) {
|
||||
// is detected by the same comparison.
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
@@ -397,9 +373,6 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
// Force a model rebuild on the next frame as glyph coordinates from a
|
||||
// previous atlas are meaningless in the new one.
|
||||
this._lastAtlasPagesVersion = -1;
|
||||
this.invalidateAtlasTextures();
|
||||
}
|
||||
|
||||
@@ -414,9 +387,8 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { RenderModelConstants } from './RenderModel';
|
||||
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
POSITION = 0,
|
||||
@@ -90,23 +89,21 @@ export class RectangleRenderer extends Disposable {
|
||||
private _terminal: Terminal,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _logService: ILogService
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
const gl = this._gl;
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource, this._logService));
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
this._register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
const vertexArrayObject = this._vertexArrayObject = gl.createVertexArray();
|
||||
this._register(toDisposable(() => gl.deleteVertexArray(vertexArrayObject)));
|
||||
gl.bindVertexArray(vertexArrayObject);
|
||||
this._vertexArrayObject = gl.createVertexArray();
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
|
||||
@@ -69,7 +69,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
private _overflowSizePage: AtlasPage | undefined;
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transferred to the atlas.
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
@@ -109,7 +109,6 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
page.canvas.remove();
|
||||
}
|
||||
this._onAddTextureAtlasCanvas.dispose();
|
||||
this._onRemoveTextureAtlasCanvas.dispose();
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
@@ -132,16 +131,12 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Incremented whenever the structure of the pages array changes in a way that
|
||||
* invalidates texture page indexes/coordinates cached outside the atlas (page
|
||||
* merges, overflow page creation). Renderers compare this against their own
|
||||
* last-seen value to decide when to rebuild their models; the atlas is shared
|
||||
* across terminals, so a consume-once flag would only repair the first
|
||||
* renderer to draw (see the shared-atlas garble in vscode#322756).
|
||||
*/
|
||||
private _pagesVersion = 0;
|
||||
public get pagesVersion(): number { return this._pagesVersion; }
|
||||
private _requestClearModel = false;
|
||||
public beginFrame(): boolean {
|
||||
const result = this._requestClearModel;
|
||||
this._requestClearModel = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
|
||||
@@ -156,12 +151,12 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
|
||||
private _createNewPage(): AtlasPage {
|
||||
// Try merge the set of the 4 most used pages of the largest size. This is deferred to a
|
||||
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
|
||||
// microtask to ensure it does not interrupt textures that will be rendered in the current
|
||||
// animation frame which would result in blank rendered areas. This is actually not that
|
||||
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
||||
if (TextureAtlas.maxAtlasPages && this._pages.length >= Math.max(4, TextureAtlas.maxAtlasPages)) {
|
||||
// Find the set of the largest images, below the maximum size, with the highest
|
||||
// Find the set of the largest 4 images, below the maximum size, with the highest
|
||||
// percentages used
|
||||
const pagesBySize = this._pages.filter(e => {
|
||||
return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
|
||||
@@ -171,55 +166,48 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
return b.percentageUsed - a.percentageUsed;
|
||||
});
|
||||
|
||||
// Find the largest group (up to 4) of same-sized pages, preferring larger page sizes. A
|
||||
// group of 2 or 3 still reduces (or at worst maintains) the page count net of the new page
|
||||
// pushed below; growing past the cap is never an option as GlyphRenderer's texture and
|
||||
// shader sampler arrays are sized to exactly maxAtlasPages (exceeding them crashes render).
|
||||
let bestGroupStart = -1;
|
||||
let bestGroupSize = 0;
|
||||
let groupStart = 0;
|
||||
for (let i = 1; i <= pagesBySize.length; i++) {
|
||||
if (i === pagesBySize.length || pagesBySize[i].canvas.width !== pagesBySize[groupStart].canvas.width) {
|
||||
const groupSize = Math.min(4, i - groupStart);
|
||||
if (groupSize > bestGroupSize) {
|
||||
bestGroupSize = groupSize;
|
||||
bestGroupStart = groupStart;
|
||||
if (groupSize === 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
groupStart = i;
|
||||
let sameSizeI = -1;
|
||||
let size = 0;
|
||||
for (let i = 0; i < pagesBySize.length; i++) {
|
||||
if (pagesBySize[i].canvas.width !== size) {
|
||||
sameSizeI = i;
|
||||
size = pagesBySize[i].canvas.width;
|
||||
} else if (i - sameSizeI === 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestGroupSize >= 2) {
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(bestGroupStart, bestGroupStart + bestGroupSize);
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => this._pages.indexOf(e)).sort((a, b) => a - b);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
this._deletePage(sortedMergingPagesIndexes[i]);
|
||||
}
|
||||
|
||||
// Add the new merged page to the end
|
||||
this.pages.push(mergedPage);
|
||||
|
||||
// Invalidate models so all texture pages are refreshed.
|
||||
this._pagesVersion++;
|
||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||
} else {
|
||||
// No two same-sized pages below the maximum size exist, so merging cannot reduce the
|
||||
// page count. Evict all pages rather than exceed the renderers' fixed texture/sampler
|
||||
// capacity; glyphs re-rasterize on demand and models rebuild via pagesVersion.
|
||||
this._evictAllPages();
|
||||
// Only proceed with merge if we have exactly 4 same-sized pages. If not, we cannot
|
||||
// effectively reduce page count and merging would cause issues.
|
||||
if (mergingPages.length < 4 || mergingPages.some(p => p.canvas.width !== mergingPages[0].canvas.width)) {
|
||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||
this._pages.push(newPage);
|
||||
this._activePages.push(newPage);
|
||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
||||
return newPage;
|
||||
}
|
||||
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version = ++AtlasPage.nextVersion;
|
||||
|
||||
// Delete the pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 0; i--) {
|
||||
this._deletePage(sortedMergingPagesIndexes[i]);
|
||||
}
|
||||
|
||||
// Add the new merged page to the end
|
||||
this.pages.push(mergedPage);
|
||||
|
||||
// Request the model to be cleared to refresh all texture pages.
|
||||
this._requestClearModel = true;
|
||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||
}
|
||||
|
||||
// All new atlas pages are created small as they are highly dynamic
|
||||
@@ -269,27 +257,6 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes every page and clears the glyph caches. This is the last resort when the page cap is
|
||||
* reached but no same-sized pages exist to merge: the page count must never exceed
|
||||
* maxAtlasPages because renderers size their texture and shader sampler arrays to exactly
|
||||
* that. Glyphs re-rasterize on demand and renderers rebuild their models via
|
||||
* {@link pagesVersion}.
|
||||
*/
|
||||
private _evictAllPages(): void {
|
||||
for (const page of this._pages) {
|
||||
this._onRemoveTextureAtlasCanvas.fire(page.canvas);
|
||||
page.canvas.remove();
|
||||
}
|
||||
this._pages.length = 0;
|
||||
this._activePages.length = 0;
|
||||
this._overflowSizePage = undefined;
|
||||
this._cacheMap.clear();
|
||||
this._cacheMapCombined.clear();
|
||||
this._didWarmUp = false;
|
||||
this._pagesVersion++;
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean, domContainer: HTMLElement | undefined): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext, restrictToCellHeight, domContainer);
|
||||
}
|
||||
@@ -562,7 +529,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
let customGlyph = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
const variantOffset = this._workAttributeData.getUnderlineVariantOffset();
|
||||
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, this._logService, backgroundColor.css, variantOffset);
|
||||
customGlyph = tryDrawCustomGlyph(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.deviceCharWidth, this._config.deviceCharHeight, this._config.fontSize, this._config.devicePixelRatio, backgroundColor.css, variantOffset);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
@@ -850,8 +817,8 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
this._overflowSizePage = new AtlasPage(this._document, this._config.deviceMaxTextureSize);
|
||||
this.pages.push(this._overflowSizePage);
|
||||
|
||||
// Invalidate models so all texture pages are refreshed.
|
||||
this._pagesVersion++;
|
||||
// Request the model to be cleared to refresh all texture pages.
|
||||
this._requestClearModel = true;
|
||||
this._onAddTextureAtlasCanvas.fire(this._overflowSizePage.canvas);
|
||||
}
|
||||
activePage = this._overflowSizePage;
|
||||
@@ -1112,33 +1079,17 @@ class AtlasPage {
|
||||
size: number,
|
||||
sourcePages?: AtlasPage[]
|
||||
) {
|
||||
if (sourcePages) {
|
||||
for (const p of sourcePages) {
|
||||
this._glyphs.push(...p.glyphs);
|
||||
this._usedPixels += p._usedPixels;
|
||||
}
|
||||
}
|
||||
this.canvas = createCanvas(document, size, size);
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
|
||||
if (sourcePages) {
|
||||
if (sourcePages.length === 4) {
|
||||
// optimized for quadmerge
|
||||
this._glyphs = this._glyphs.concat(
|
||||
sourcePages[0].glyphs,
|
||||
sourcePages[1].glyphs,
|
||||
sourcePages[2].glyphs,
|
||||
sourcePages[3].glyphs
|
||||
);
|
||||
this._usedPixels = sourcePages[0]._usedPixels +
|
||||
sourcePages[1]._usedPixels +
|
||||
sourcePages[2]._usedPixels +
|
||||
sourcePages[3]._usedPixels;
|
||||
} else {
|
||||
// 2- and 3-page merges, used when no 4-page same-size set exists at the page cap
|
||||
for (let i = 0; i < sourcePages.length; ++i) {
|
||||
this._glyphs = this._glyphs.concat(sourcePages[i].glyphs);
|
||||
this._usedPixels += sourcePages[i]._usedPixels;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
@@ -1173,7 +1124,7 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThreshol
|
||||
// were covered (fg=#8ae234, bg=#c4a000).
|
||||
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
|
||||
|
||||
// Set alpha channel of relevant pixels to 0
|
||||
// Set alpha channel of relevent pixels to 0
|
||||
let isEmpty = true;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
// Check exact match
|
||||
|
||||
@@ -28,7 +28,6 @@ export interface ICursorRenderModel {
|
||||
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
createVertexArray(): IWebGLVertexArrayObject;
|
||||
deleteVertexArray(vao: IWebGLVertexArrayObject): void;
|
||||
bindVertexArray(vao: IWebGLVertexArrayObject): void;
|
||||
drawElementsInstanced(mode: number, count: number, type: number, offset: number, instanceCount: number): void;
|
||||
}
|
||||
@@ -68,13 +67,10 @@ export interface ITextureAtlas extends IDisposable {
|
||||
warmUp(): void;
|
||||
|
||||
/**
|
||||
* Incremented whenever the pages array changes structurally (page merge,
|
||||
* overflow page creation), invalidating any texture page indexes/coordinates
|
||||
* cached by renderers. Tracked as a version rather than a consumable flag
|
||||
* because the atlas may be shared by multiple renderers, each of which must
|
||||
* notice the change independently.
|
||||
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
|
||||
* for a new set of glyphs.
|
||||
*/
|
||||
readonly pagesVersion: number;
|
||||
beginFrame(): boolean;
|
||||
|
||||
/**
|
||||
* Clear all glyphs from the texture atlas.
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRender
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { getSafariVersion, isSafari } from 'common/Platform';
|
||||
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { Emitter, EventUtils } from 'common/Event';
|
||||
@@ -40,7 +40,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
||||
};
|
||||
const gl = document.createElement('canvas').getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
||||
if (!gl) {
|
||||
throw new Error('WebGL2 is only supported on Safari 16 and above');
|
||||
throw new Error('Webgl2 is only supported on Safari 16 and above');
|
||||
}
|
||||
}
|
||||
super();
|
||||
@@ -65,7 +65,6 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const logService: ILogService = unsafeCore._logService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
this._renderer = this._register(new WebglRenderer(
|
||||
@@ -75,7 +74,6 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
||||
coreBrowserService,
|
||||
coreService,
|
||||
decorationService,
|
||||
logService,
|
||||
optionsService,
|
||||
themeService,
|
||||
this._customGlyphs,
|
||||
|
||||
@@ -10,12 +10,12 @@ import { CursorBlinkStateManager } from './CursorBlinkStateManager';
|
||||
import { observeDevicePixelDimensions } from './DevicePixelObserver';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/buffer/Types';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
|
||||
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from '@xterm/xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
@@ -77,7 +77,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
private readonly _coreService: ICoreService,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _logService: ILogService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _customGlyphs: boolean = true,
|
||||
@@ -123,19 +122,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._deviceMaxTextureSize = this._gl.getParameter(this._gl.MAX_TEXTURE_SIZE);
|
||||
|
||||
this._register(addDisposableListener(this._canvas, 'webglcontextlost', (e) => {
|
||||
this._logService.debug('webglcontextlost event received');
|
||||
console.log('webglcontextlost event received');
|
||||
// Prevent the default behavior in order to enable WebGL context restoration.
|
||||
e.preventDefault();
|
||||
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
|
||||
// If not, dispatch the onContextLoss notification to observers.
|
||||
this._contextRestorationTimeout = setTimeout(() => {
|
||||
this._contextRestorationTimeout = undefined;
|
||||
this._logService.warn('webgl context not restored; firing onContextLoss');
|
||||
console.warn('webgl context not restored; firing onContextLoss');
|
||||
this._onContextLoss.fire(e);
|
||||
}, 3000 /* ms */);
|
||||
}));
|
||||
this._register(addDisposableListener(this._canvas, 'webglcontextrestored', (e) => {
|
||||
this._logService.warn('webglcontextrestored event received');
|
||||
console.warn('webglcontextrestored event received');
|
||||
clearTimeout(this._contextRestorationTimeout);
|
||||
this._contextRestorationTimeout = undefined;
|
||||
// The texture atlas and glyph renderer must be fully reinitialized
|
||||
@@ -159,8 +158,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._isAttached = this._core.screenElement!.isConnected;
|
||||
|
||||
this._register(toDisposable(() => {
|
||||
clearTimeout(this._contextRestorationTimeout);
|
||||
this._contextRestorationTimeout = undefined;
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
@@ -277,8 +274,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
* Initializes members dependent on WebGL context state.
|
||||
*/
|
||||
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
|
||||
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService, this._logService);
|
||||
this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService, this._logService);
|
||||
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService);
|
||||
this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService);
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.handleCharSizeChanged();
|
||||
@@ -427,8 +424,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
let line: IBufferLine;
|
||||
let joinedRanges: [number, number][];
|
||||
let isJoined: boolean;
|
||||
let skipJoinedCheckUntilX: number;
|
||||
let isValidJoinRange: boolean;
|
||||
let skipJoinedCheckUntilX: number = 0;
|
||||
let isValidJoinRange: boolean = true;
|
||||
let lastCharX: number;
|
||||
let range: [number, number];
|
||||
let isCursorRow: boolean;
|
||||
@@ -456,18 +453,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
for (y = start; y <= end; y++) {
|
||||
row = y + terminal.buffer.ydisp;
|
||||
const bufferLine = terminal.buffer.lines.get(row);
|
||||
if (!bufferLine) {
|
||||
this._model.lineLengths[y] = 0;
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||
modelUpdated = true;
|
||||
this._nullModelCell(x, y, j, 0, 0, 0);
|
||||
}
|
||||
this._setRowBlinkState(y, false);
|
||||
continue;
|
||||
}
|
||||
line = bufferLine;
|
||||
line = terminal.buffer.lines.get(row)!;
|
||||
let rowHasBlinkingCells = false;
|
||||
this._model.lineLengths[y] = 0;
|
||||
isCursorRow = cursorY === row;
|
||||
@@ -598,9 +584,13 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Null out non-first cells
|
||||
for (x++; x <= lastCharX; x++) {
|
||||
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
|
||||
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
|
||||
// supported
|
||||
this._nullModelCell(x, y, j, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||
this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
}
|
||||
x--; // Go back to the previous update cell for next iteration
|
||||
}
|
||||
@@ -614,14 +604,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateTextBlinkState();
|
||||
}
|
||||
|
||||
private _nullModelCell(x: number, y: number, cellIndex: number, bg: number, fg: number, ext: number): void {
|
||||
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, bg, fg, ext, NULL_CELL_CHAR, 0, 0);
|
||||
this._model.cells[cellIndex] = NULL_CELL_CODE;
|
||||
this._model.cells[cellIndex + RenderModelConstants.BG_OFFSET] = bg;
|
||||
this._model.cells[cellIndex + RenderModelConstants.FG_OFFSET] = fg;
|
||||
this._model.cells[cellIndex + RenderModelConstants.EXT_OFFSET] = ext;
|
||||
}
|
||||
|
||||
private _resetBlinkingRowState(): void {
|
||||
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
|
||||
this._rowHasBlinkingCellsCount = 0;
|
||||
@@ -679,10 +661,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
||||
|
||||
// The size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// and not ceils as browsers often have floating point precision issues where
|
||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||
// it's actually 1.1. Ceiling may cause blurriness as the backing canvas image is 1 pixel too
|
||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
||||
// large for the canvas element size.
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
|
||||
/**
|
||||
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
|
||||
@@ -17,21 +16,21 @@ export const PROJECTION_MATRIX = new Float32Array([
|
||||
-1, 1, 0, 1
|
||||
]);
|
||||
|
||||
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string, logService: ILogService): WebGLProgram | undefined {
|
||||
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined {
|
||||
const program = throwIfFalsy(gl.createProgram());
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource, logService)));
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource, logService)));
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource)));
|
||||
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)));
|
||||
gl.linkProgram(program);
|
||||
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
|
||||
if (success) {
|
||||
return program;
|
||||
}
|
||||
|
||||
logService.error(gl.getProgramInfoLog(program));
|
||||
console.error(gl.getProgramInfoLog(program));
|
||||
gl.deleteProgram(program);
|
||||
}
|
||||
|
||||
export function createShader(gl: WebGLRenderingContext, type: number, source: string, logService: ILogService): WebGLShader | undefined {
|
||||
export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
|
||||
const shader = throwIfFalsy(gl.createShader(type));
|
||||
gl.shaderSource(shader, source);
|
||||
gl.compileShader(shader);
|
||||
@@ -40,7 +39,7 @@ export function createShader(gl: WebGLRenderingContext, type: number, source: st
|
||||
return shader;
|
||||
}
|
||||
|
||||
logService.error(gl.getShaderInfoLog(shader));
|
||||
console.error(gl.getShaderInfoLog(shader));
|
||||
gl.deleteShader(shader);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphCharacterDefinition, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition } from './Types';
|
||||
|
||||
/* oxlint-disable @stylistic/max-len -- generated glyph path data */
|
||||
/* eslint-disable max-len */
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import type { ILogService } from 'common/services/Services';
|
||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
|
||||
@@ -23,7 +22,6 @@ export function tryDrawCustomGlyph(
|
||||
deviceCharHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number,
|
||||
logService: ILogService,
|
||||
backgroundColor?: string,
|
||||
variantOffset: number = 0
|
||||
): boolean {
|
||||
@@ -32,7 +30,7 @@ export function tryDrawCustomGlyph(
|
||||
// Normalize to array for uniform handling
|
||||
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
|
||||
for (const part of parts) {
|
||||
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, logService, backgroundColor, variantOffset);
|
||||
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, backgroundColor, variantOffset);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -51,7 +49,6 @@ function drawDefinitionPart(
|
||||
deviceCharHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number,
|
||||
logService: ILogService,
|
||||
backgroundColor?: string,
|
||||
variantOffset: number = 0
|
||||
): void {
|
||||
@@ -82,7 +79,7 @@ function drawDefinitionPart(
|
||||
drawPatternChar(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, variantOffset);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, logService, part.strokeWidth);
|
||||
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH:
|
||||
drawPathDefinitionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
|
||||
@@ -91,7 +88,7 @@ function drawDefinitionPart(
|
||||
drawPathNegativeDefinitionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, backgroundColor);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
||||
drawVectorShape(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, fontSize, devicePixelRatio, logService);
|
||||
drawVectorShape(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, fontSize, devicePixelRatio);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.BRAILLE:
|
||||
drawBrailleCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
|
||||
@@ -221,8 +218,8 @@ function drawPathDefinitionCharacter(
|
||||
const rx = parseFloat(args[0]) * deviceCellWidth;
|
||||
const ry = parseFloat(args[1]) * deviceCellHeight;
|
||||
const xAxisRotation = parseFloat(args[2]) * Math.PI / 180;
|
||||
const largeArcFlag = parseInt(args[3], 10);
|
||||
const sweepFlag = parseInt(args[4], 10);
|
||||
const largeArcFlag = parseInt(args[3]);
|
||||
const sweepFlag = parseInt(args[4]);
|
||||
const x = xOffset + parseFloat(args[5]) * deviceCellWidth;
|
||||
const y = yOffset + parseFloat(args[6]) * deviceCellHeight;
|
||||
drawSvgArc(ctx, currentX, currentY, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x, y);
|
||||
@@ -513,7 +510,6 @@ function drawPathFunctionCharacter(
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
devicePixelRatio: number,
|
||||
logService: ILogService,
|
||||
strokeWidth?: number
|
||||
): void {
|
||||
ctx.save();
|
||||
@@ -540,7 +536,7 @@ function drawPathFunctionCharacter(
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
logService.error(`Could not find drawing instructions for "${type}"`);
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
@@ -602,8 +598,7 @@ function drawVectorShape(
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number,
|
||||
logService: ILogService
|
||||
devicePixelRatio: number
|
||||
): void {
|
||||
// Clip the cell to make sure drawing doesn't occur beyond bounds
|
||||
const clipRegion = new Path2D();
|
||||
@@ -624,7 +619,7 @@ function drawVectorShape(
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
logService.error(`Could not find drawing instructions for "${type}"`);
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"experimentalDecorators": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Reproduces the single-terminal WebGL crash/garble when the texture atlas
|
||||
* page count overflows TextureAtlas.maxAtlasPages (vscode#322756 class).
|
||||
*
|
||||
* GlyphRenderer sizes its GL texture array and fragment-shader sampler array
|
||||
* to exactly maxAtlasPages at construction time, but TextureAtlas._createNewPage
|
||||
* pushes past that cap whenever a 4-page same-size merge is impossible (e.g. a
|
||||
* heterogeneous mix like 1024+512+512+512). The next GlyphRenderer.render then
|
||||
* throws "Cannot read properties of undefined (reading 'version')" and cells
|
||||
* reference sampler indexes the shader does not have, freezing/garbling the
|
||||
* terminal. Long heavy-CLI sessions hit this in production; the test makes it
|
||||
* fast by building the renderer with a small cap (the same code path as the
|
||||
* production 8/16/32 values).
|
||||
*
|
||||
* Chromium only (additive scope; Firefox/WebKit WebGL broken in CI, xtermjs#5854).
|
||||
*/
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
loadWebglStrict,
|
||||
resetMaxAtlasPages,
|
||||
setMaxAtlasPages,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
const HEADER = ' OVERFLOW_REF_0123456789';
|
||||
const HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
test.describe('atlas page overflow (#322756 class)', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||
test.describe.configure({ timeout: 60000 });
|
||||
|
||||
test('renderer survives the atlas exceeding maxAtlasPages-sized textures', async ({ browser }) => {
|
||||
const ctx: ITestContext = await createTestContext(browser);
|
||||
const errors: string[] = [];
|
||||
const onError = (e: Error): void => { errors.push(e.message); };
|
||||
ctx.page.on('pageerror', onError);
|
||||
try {
|
||||
// Bootstrap: create a throwaway WebGL terminal so the TextureAtlas class
|
||||
// statics exist, then lower the page cap. openTerminal() disposes the old
|
||||
// terminal (and its atlas), so the next WebGL renderer is CONSTRUCTED
|
||||
// with the small cap: its GL texture array and fragment-shader sampler
|
||||
// array have exactly 4 entries, faithfully mirroring production where
|
||||
// they always equal maxAtlasPages.
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
|
||||
const atlasTexturesLength = await ctx.page.evaluate(() =>
|
||||
(window as any).term._core._renderService._renderer.value._glyphRenderer.value._atlasTextures.length);
|
||||
expect(atlasTexturesLength, 'renderer must be built with the lowered page cap').toBe(4);
|
||||
|
||||
// Pinned header in the alt buffer; body churns distinct CJK glyphs below
|
||||
// (cursor-positioned, no trailing linefeed on the bottom row -> no scroll).
|
||||
await writeAndWaitForRender(ctx, `\x1b[?1049h\x1b[H${HEADER}`);
|
||||
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||
await ctx.page.locator('#terminal-container .xterm-screen').screenshot({ path: 'out-esbuild-test/playwright/overflow-before.png' });
|
||||
|
||||
const glyphsPerChunk = 23 * 40 - 1;
|
||||
for (let c = 0; c < 16; c++) {
|
||||
// Plain write + short wait instead of waiting for a render event: once
|
||||
// the overflow crash fires, render events stop arriving and a render
|
||||
// wait would hang the test instead of failing it.
|
||||
await ctx.proxy.write('\x1b[2;1H' + generateUniqueGlyphFlood(glyphsPerChunk, 80, c * glyphsPerChunk));
|
||||
await ctx.page.waitForTimeout(50);
|
||||
}
|
||||
await ctx.page.waitForTimeout(300);
|
||||
await ctx.page.locator('#terminal-container .xterm-screen').screenshot({ path: 'out-esbuild-test/playwright/overflow-after.png' });
|
||||
|
||||
const state = await ctx.page.evaluate(() => {
|
||||
const renderer = (window as any).term._core._renderService._renderer.value;
|
||||
return {
|
||||
pages: renderer._charAtlas.pages.length,
|
||||
atlasTextures: renderer._glyphRenderer.value._atlasTextures.length
|
||||
};
|
||||
});
|
||||
console.error('[overflow] pages=', state.pages, 'atlasTextures=', state.atlasTextures, 'pageErrors=', errors.length, errors[0] ?? '');
|
||||
|
||||
// Contract: the page count must never exceed the renderer's texture and
|
||||
// sampler capacity, and rendering must never throw.
|
||||
expect(errors, `GlyphRenderer must not crash when the atlas is saturated: ${errors[0] ?? ''}`).toEqual([]);
|
||||
expect(state.pages, 'atlas page count must stay within the renderer texture capacity').toBeLessThanOrEqual(state.atlasTextures);
|
||||
|
||||
// The untouched header must still render correctly after the churn.
|
||||
const after = await captureRowSignatures(ctx, 1, HEADER_COLS);
|
||||
for (let i = 0; i < HEADER_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `header col ${HEADER_COLS[i]} corrupted after atlas saturation`);
|
||||
}
|
||||
} finally {
|
||||
ctx.page.off('pageerror', onError);
|
||||
// Restore the default so a lowered cap can never leak into other tests.
|
||||
await resetMaxAtlasPages(ctx).catch(() => { /* page may already be unusable after a crash */ });
|
||||
await ctx.page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,109 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* WebGL device-pixel-ratio (HiDPI) and font-size ("resize") atlas tests.
|
||||
*
|
||||
* A devicePixelRatio > 1 and font-size changes both change the device cell
|
||||
* dimensions, which key the texture atlas cache and force the atlas to be
|
||||
* rebuilt. Historically these paths have produced blank/oversized/garbled glyphs
|
||||
* (e.g. xtermjs#4728, #5915/#5929). These tests assert glyphs render and stay
|
||||
* correct across those transitions.
|
||||
*
|
||||
* Chromium + SwiftShader only (additive scope; Firefox/WebKit WebGL broken in CI
|
||||
* per xtermjs#5854).
|
||||
*/
|
||||
|
||||
import test, { Browser, expect } from '@playwright/test';
|
||||
import { ITestContext, TerminalProxy, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
assertTextureAtlasPresent,
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
loadWebglStrict,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
|
||||
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
/**
|
||||
* Creates a test context whose page uses a specific devicePixelRatio. The shared
|
||||
* `createTestContext` helper always uses DPR 1 (`browser.newPage()`), so HiDPI
|
||||
* tests build their own context here.
|
||||
*/
|
||||
async function createDprContext(browser: Browser, deviceScaleFactor: number): Promise<ITestContext> {
|
||||
const context = await browser.newContext({ deviceScaleFactor });
|
||||
const page = await context.newPage();
|
||||
page.on('pageerror', e => console.error(`[dpr ${deviceScaleFactor}]`, e));
|
||||
await page.goto('/test');
|
||||
const proxy = new TerminalProxy(page);
|
||||
proxy.initPage();
|
||||
return {
|
||||
browser,
|
||||
page,
|
||||
termHandle: await page.evaluateHandle('window.term'),
|
||||
proxy
|
||||
};
|
||||
}
|
||||
|
||||
test.describe('WebGL HiDPI (devicePixelRatio) rendering', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
|
||||
test('initializes the WebGL renderer and scales to device pixels at devicePixelRatio 2', async ({ browser }) => {
|
||||
const ctx = await createDprContext(browser, 2);
|
||||
try {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
// GPU must be genuinely active at HiDPI (asserted inside loadWebglStrict).
|
||||
await loadWebglStrict(ctx);
|
||||
await writeAndWaitForRender(ctx, 'A W .');
|
||||
await assertTextureAtlasPresent(ctx);
|
||||
|
||||
// The WebGL renderer must render at the device resolution: device cell
|
||||
// dimensions should be ~2x the CSS cell dimensions. This catches the
|
||||
// DPR-scaling class of bugs (e.g. xtermjs#4728) without relying on
|
||||
// screenshotting a WebGL canvas at a non-1 device scale factor.
|
||||
const dims = await ctx.proxy.core.renderDimensions;
|
||||
expect(dims.device.cell.width).toBeGreaterThan(dims.css.cell.width * 1.8);
|
||||
expect(dims.device.cell.width).toBeLessThan(dims.css.cell.width * 2.2);
|
||||
expect(dims.device.cell.height).toBeGreaterThan(dims.css.cell.height * 1.8);
|
||||
expect(dims.device.cell.height).toBeLessThan(dims.css.cell.height * 2.2);
|
||||
} finally {
|
||||
await ctx.page.context().close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('WebGL font-size change atlas integrity', () => {
|
||||
let ctx: ITestContext;
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createDprContext(browser, 1);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24, fontSize: 15 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
test('glyphs stay correct after cycling font size (atlas rebuilds)', async () => {
|
||||
await writeAndWaitForRender(ctx, 'FontRef0123456789');
|
||||
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
|
||||
// Changing font size changes device cell dimensions -> the atlas is rebuilt.
|
||||
for (const fontSize of [12, 18, 21, 15]) {
|
||||
await ctx.proxy.setOption('fontSize', fontSize);
|
||||
await writeAndWaitForRender(ctx, '\r\nchurn');
|
||||
}
|
||||
|
||||
// Back at the original font size the reference glyphs must be unchanged.
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 16, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after font-size cycling`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,121 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* WebGL glyph + texture-atlas integrity tests.
|
||||
*
|
||||
* These tests verify that *glyphs* render correctly (not just that cell colors
|
||||
* are right) under conditions that stress the texture atlas: multiple atlas
|
||||
* pages, page merges and atlas clears. The goal is to catch the "garbled glyph"
|
||||
* corruption class reported in microsoft/vscode#322756.
|
||||
*
|
||||
* Note: CI runs WebGL on Chromium + SwiftShader (software WebGL). That catches
|
||||
* renderer/atlas *logic* corruption (stale texture pages, missing model clears),
|
||||
* but not GPU-driver-specific glitches. These tests target the former.
|
||||
*/
|
||||
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
assertTextureAtlasPresent,
|
||||
captureRowSignatures,
|
||||
expectSignatureDiffers,
|
||||
expectSignatureMatches,
|
||||
floodDistinctGlyphs,
|
||||
loadWebglStrict,
|
||||
resetMaxAtlasPages,
|
||||
setMaxAtlasPages,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('WebGL glyph + atlas integrity', () => {
|
||||
// Additive scope: only run on Chromium (SwiftShader). Firefox/WebKit WebGL
|
||||
// tests are known-broken in CI (xtermjs#5854) and out of scope here.
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
|
||||
test.beforeEach(async () => {
|
||||
// Fresh terminal (and therefore a predictable atlas) per test.
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
// A lowered page cap must not leak into renderers built by later tests.
|
||||
test.afterEach(async () => await resetMaxAtlasPages(ctx));
|
||||
|
||||
test('renders distinct, non-blank glyphs', async () => {
|
||||
await writeAndWaitForRender(ctx, 'A W .');
|
||||
await assertTextureAtlasPresent(ctx);
|
||||
|
||||
const [a, w, dot, blank] = await captureRowSignatures(ctx, 1, [1, 3, 5, 40]);
|
||||
// A written cell must differ from an empty cell (glyph actually drawn).
|
||||
expectSignatureDiffers(a, blank, 24, 'glyph "A" should not be blank');
|
||||
expectSignatureDiffers(w, blank, 24, 'glyph "W" should not be blank');
|
||||
// Different glyphs must look different (signature has discriminating power).
|
||||
expectSignatureDiffers(a, w, 24, 'glyphs "A" and "W" should render differently');
|
||||
expectSignatureDiffers(a, dot, 24, 'glyphs "A" and "." should render differently');
|
||||
});
|
||||
|
||||
test('glyphs on the first atlas page stay correct after more pages are allocated', async () => {
|
||||
// Reference: a known row of glyphs that lands on the first atlas page.
|
||||
await writeAndWaitForRender(ctx, 'REFERENCE_LINE_0123456789\r\n');
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const reference = await captureRowSignatures(ctx, 1, cols);
|
||||
|
||||
// Flood thousands of distinct glyphs (one screenful rendered at a time, so
|
||||
// they actually rasterize) to force multiple atlas pages.
|
||||
const pages = await floodDistinctGlyphs(ctx, 4);
|
||||
expect(pages, 'flooding distinct glyphs should allocate more than one atlas page').toBeGreaterThan(1);
|
||||
|
||||
// Bring the reference line back into view and re-check its glyphs.
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${cols[i]} corrupted after multi-page allocation`);
|
||||
}
|
||||
});
|
||||
|
||||
test('glyphs stay correct after a texture-atlas page merge', async () => {
|
||||
// Force merges to happen almost immediately.
|
||||
await writeAndWaitForRender(ctx, 'MERGE_REF_LINE_0123456789\r\n');
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const reference = await captureRowSignatures(ctx, 1, cols);
|
||||
|
||||
// Flood distinct glyphs (rendered per screenful) to exceed the tiny page cap
|
||||
// and trigger merges.
|
||||
const pages = await floodDistinctGlyphs(ctx, 12);
|
||||
expect(pages, 'flood should have allocated multiple atlas pages (and merged)').toBeGreaterThan(1);
|
||||
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${cols[i]} corrupted after atlas page merge`);
|
||||
}
|
||||
});
|
||||
|
||||
test('glyphs re-render correctly after clearTextureAtlas() on the same terminal', async () => {
|
||||
await writeAndWaitForRender(ctx, 'CLEAR_REF_LINE_0123456789');
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const reference = await captureRowSignatures(ctx, 1, cols);
|
||||
|
||||
await ctx.proxy.clearTextureAtlas();
|
||||
// Force a full redraw so glyphs are re-rasterized into the cleared atlas.
|
||||
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
||||
await waitForRender(ctx);
|
||||
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `glyph at col ${cols[i]} corrupted after clearTextureAtlas()`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Single-terminal guard for the vscode#322756 "garbled glyph" class: a pinned
|
||||
* alt-buffer header must render identically while the body churns enough
|
||||
* distinct colored glyphs to force texture-atlas page merges.
|
||||
*
|
||||
* Note this scenario is GREEN on current master: the single-terminal mid-frame
|
||||
* merge race was fixed upstream (merge-retry loop in WebglRenderer.renderRows +
|
||||
* globally monotonic AtlasPage versions). The test asserts the buffer content
|
||||
* first so a content bug (e.g. an accidental scroll from the churn stream) can
|
||||
* never masquerade as renderer corruption.
|
||||
*/
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import { captureRowSignatures, expectSignatureMatches, getAtlasPageCount, loadWebglStrict, resetMaxAtlasPages, setMaxAtlasPages, writeAndWaitForRender } from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateColoredAsciiFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
const HEADER = ' HEADER_REF_0123456789';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); });
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('single-terminal merge integrity (#322756 class)', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||
test.describe.configure({ timeout: 60000 });
|
||||
|
||||
// A lowered page cap must not leak into renderers built by later tests.
|
||||
test.afterEach(async () => await resetMaxAtlasPages(ctx));
|
||||
|
||||
test('pinned header renders identically across 30 merge-heavy body redraws', async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
// Alt-buffer TUI: pinned header on row 1, body churns below (like Copilot CLI).
|
||||
await writeAndWaitForRender(ctx, `\x1b[?1049h\x1b[H${HEADER}`);
|
||||
const ref = await captureRowSignatures(ctx, 1, cols);
|
||||
let maxDiff = 0;
|
||||
for (let c = 0; c < 30; c++) {
|
||||
// Redraw body rows 2..24 with unique glyphs; the header row is never touched.
|
||||
let frame = '\x1b[2;1H';
|
||||
frame += generateColoredAsciiFlood(23 * 78, c * 23 * 78);
|
||||
await writeAndWaitForRender(ctx, frame);
|
||||
const now = await captureRowSignatures(ctx, 1, cols); // forces GPU sync
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
const d = Math.max(...ref[i].map((v, k) => Math.abs(v - now[i][k])));
|
||||
maxDiff = Math.max(maxDiff, d);
|
||||
}
|
||||
}
|
||||
console.error('[repro] pages=', await getAtlasPageCount(ctx), 'headerMaxDiff=', maxDiff);
|
||||
|
||||
// Guard: the header must still be in the buffer. If this fails the churn
|
||||
// stream scrolled the screen and any pixel diff is meaningless.
|
||||
const row1Text = await ctx.page.evaluate(() => (window as any).term.buffer.active.getLine(0)?.translateToString(true));
|
||||
expect(row1Text, 'churn stream must not scroll the pinned header out of the buffer').toBe(HEADER);
|
||||
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], ref[i], 14, `header col ${cols[i]} garbled`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,159 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Reproduces the vscode#322756 "garbled glyph" corruption across terminals that
|
||||
* SHARE a texture atlas (CharAtlasCache hands the same TextureAtlas to every
|
||||
* terminal with an equal config — in VS Code that is every integrated terminal
|
||||
* using the same font settings, including task/background terminals).
|
||||
*
|
||||
* Mechanism: when the atlas merges pages it raises a consume-once
|
||||
* "clear model" flag (TextureAtlas.beginFrame). Only the FIRST terminal to
|
||||
* render consumes it and rebuilds its vertex model; every other terminal keeps
|
||||
* stale texture-page indexes/coordinates in its vertex buffer and draws wrong
|
||||
* glyphs on its next partial render, even though its buffer content is intact.
|
||||
*
|
||||
* The DOM-renderer control shows the corruption is exclusive to the GPU path
|
||||
* (`terminal.integrated.gpuAcceleration off` cannot reproduce it: the DOM
|
||||
* renderer has no texture atlas).
|
||||
*
|
||||
* Chromium only (additive scope; Firefox/WebKit WebGL broken in CI, xtermjs#5854).
|
||||
*/
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
getAtlasPageCount,
|
||||
loadWebglStrict,
|
||||
setMaxAtlasPages,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateColoredAsciiFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
const HEADER = ' HEADER_REF_0123456789';
|
||||
const HEADER_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const TERM_B_SELECTOR = '#terminal-container-b .xterm-screen';
|
||||
|
||||
/**
|
||||
* Creates a second terminal ("termB") on the test page with the given options
|
||||
* and optionally loads the WebGL addon on it. Identical rendering options mean
|
||||
* CharAtlasCache gives it the same atlas instance as `window.term`.
|
||||
*/
|
||||
async function createTerminalB(ctx: ITestContext, options: { useWebgl: boolean }): Promise<void> {
|
||||
await ctx.page.evaluate(async (useWebgl) => {
|
||||
const w = window as any;
|
||||
w.termB?.dispose();
|
||||
document.getElementById('terminal-container-b')?.remove();
|
||||
const el = document.createElement('div');
|
||||
el.id = 'terminal-container-b';
|
||||
document.body.appendChild(el);
|
||||
w.termB = new w.Terminal({ cols: 80, rows: 24, allowProposedApi: true });
|
||||
w.termB.open(el);
|
||||
if (useWebgl) {
|
||||
w.addonB = new w.WebglAddon({ preserveDrawingBuffer: true });
|
||||
w.termB.loadAddon(w.addonB);
|
||||
}
|
||||
}, options.useWebgl);
|
||||
}
|
||||
|
||||
/** Writes data to termB and resolves after its next render event. */
|
||||
async function writeToBAndWaitForRender(ctx: ITestContext, data: string): Promise<void> {
|
||||
await ctx.page.evaluate(d => new Promise<void>(resolve => {
|
||||
const termB = (window as any).termB;
|
||||
const disposable = termB.onRender(() => {
|
||||
disposable.dispose();
|
||||
resolve();
|
||||
});
|
||||
termB.write(d);
|
||||
}), data);
|
||||
}
|
||||
|
||||
async function getTermBRow1Text(ctx: ITestContext): Promise<string | undefined> {
|
||||
return ctx.page.evaluate(() => (window as any).termB.buffer.active.getLine(0)?.translateToString(true));
|
||||
}
|
||||
|
||||
test.describe('shared-atlas garble across terminals (#322756)', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||
test.describe.configure({ timeout: 60000 });
|
||||
|
||||
test('terminal B keeps rendering its header correctly while terminal A forces atlas merges', async ({ browser }) => {
|
||||
const ctx: ITestContext = await createTestContext(browser);
|
||||
try {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
await createTerminalB(ctx, { useWebgl: true });
|
||||
|
||||
// Both terminals must be using the same atlas instance for this test to
|
||||
// mean anything.
|
||||
const atlasShared = await ctx.page.evaluate(() => {
|
||||
const w = window as any;
|
||||
const atlasA = w.term._core._renderService._renderer.value._charAtlas;
|
||||
const atlasB = w.termB._core._renderService._renderer.value._charAtlas;
|
||||
return !!atlasA && atlasA === atlasB;
|
||||
});
|
||||
expect(atlasShared, 'terminals with equal configs should share one texture atlas').toBe(true);
|
||||
|
||||
// Lower the cap AFTER both renderers exist so their shaders/texture
|
||||
// arrays keep the original size (this test targets stale vertex data,
|
||||
// not the page-overflow crash).
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
|
||||
// Render B's header pre-merge and capture its reference pixels.
|
||||
await writeToBAndWaitForRender(ctx, HEADER);
|
||||
await ctx.page.locator(TERM_B_SELECTOR).screenshot({ path: 'out-esbuild-test/playwright/termB-before.png' });
|
||||
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS, { selector: TERM_B_SELECTOR, termGlobal: 'termB' });
|
||||
|
||||
// Churn terminal A with distinct colored glyphs to force page merges in
|
||||
// the SHARED atlas. A repairs itself (it consumes the clear-model flag);
|
||||
// B is never told.
|
||||
for (let c = 0; c < 10; c++) {
|
||||
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78, c * 23 * 78));
|
||||
}
|
||||
const pages = await getAtlasPageCount(ctx);
|
||||
expect(pages, 'churn should have exercised multiple atlas pages').toBeGreaterThan(1);
|
||||
|
||||
// A tiny unrelated update to B's LAST row triggers a partial render in B.
|
||||
// B's header row is untouched; with a healthy renderer its pixels must
|
||||
// not change.
|
||||
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
|
||||
await ctx.page.locator(TERM_B_SELECTOR).screenshot({ path: 'out-esbuild-test/playwright/termB-after.png' });
|
||||
|
||||
// Guard: B's buffer content is intact, so any pixel change is renderer
|
||||
// corruption by construction.
|
||||
expect(await getTermBRow1Text(ctx), 'terminal B buffer must still contain the header').toBe(HEADER);
|
||||
|
||||
const after = await captureRowSignatures(ctx, 1, HEADER_COLS, { selector: TERM_B_SELECTOR, termGlobal: 'termB' });
|
||||
for (let i = 0; i < HEADER_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `terminal B header col ${HEADER_COLS[i]} garbled by terminal A's atlas merges`);
|
||||
}
|
||||
} finally {
|
||||
await ctx.page.close();
|
||||
}
|
||||
});
|
||||
|
||||
test('DOM renderer control: same workload cannot garble terminal B (no atlas without GPU acceleration)', async ({ browser }) => {
|
||||
const ctx: ITestContext = await createTestContext(browser);
|
||||
try {
|
||||
// No WebGL addon on either terminal -> DOM renderer -> no texture atlas.
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await createTerminalB(ctx, { useWebgl: false });
|
||||
|
||||
await writeToBAndWaitForRender(ctx, HEADER);
|
||||
const reference = await captureRowSignatures(ctx, 1, HEADER_COLS, { selector: TERM_B_SELECTOR, termGlobal: 'termB' });
|
||||
|
||||
for (let c = 0; c < 10; c++) {
|
||||
await writeAndWaitForRender(ctx, '\x1b[2;1H' + generateColoredAsciiFlood(23 * 78, c * 23 * 78));
|
||||
}
|
||||
await writeToBAndWaitForRender(ctx, '\x1b[24;1Hx');
|
||||
|
||||
expect(await getTermBRow1Text(ctx), 'terminal B buffer must still contain the header').toBe(HEADER);
|
||||
const after = await captureRowSignatures(ctx, 1, HEADER_COLS, { selector: TERM_B_SELECTOR, termGlobal: 'termB' });
|
||||
for (let i = 0; i < HEADER_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `DOM-rendered terminal B header col ${HEADER_COLS[i]} unexpectedly changed`);
|
||||
}
|
||||
} finally {
|
||||
await ctx.page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,104 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* Single-terminal WebGL atlas stress: try to reproduce the "garbled glyph"
|
||||
* corruption from microsoft/vscode#322756 in ONE terminal (no shared atlas),
|
||||
* via heavy unique-glyph churn that forces atlas page merges, both with content
|
||||
* pinned on screen (TUI-style) and mid-scroll. Chromium/SwiftShader only.
|
||||
*/
|
||||
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
floodDistinctGlyphs,
|
||||
getAtlasPageCount,
|
||||
loadWebglStrict,
|
||||
resetMaxAtlasPages,
|
||||
setMaxAtlasPages,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('WebGL single-terminal heavy render + merge', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL atlas tests run on Chromium/SwiftShader only');
|
||||
test.describe.configure({ timeout: 30000 });
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
// A lowered page cap must not leak into renderers built by later tests.
|
||||
test.afterEach(async () => await resetMaxAtlasPages(ctx));
|
||||
|
||||
test('heavy merge churn must not crash the GlyphRenderer (vscode#322756)', async () => {
|
||||
// Guard: heavy merges must not throw from GlyphRenderer.render. Note the
|
||||
// page cap here is lowered AFTER the renderer was built, so _atlasTextures
|
||||
// keeps its original (large) length and this test cannot hit the
|
||||
// page-overflow crash; that scenario needs the cap lowered BEFORE loading
|
||||
// the addon and is covered separately (WebglAtlasOverflow.test.ts).
|
||||
const errors: string[] = [];
|
||||
const onError = (e: Error): void => { errors.push(e.message); };
|
||||
ctx.page.on('pageerror', onError);
|
||||
try {
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
await floodDistinctGlyphs(ctx, 10);
|
||||
// Force a GPU read (screenshot) mid-churn so a render lands during/after a
|
||||
// merge, then churn more — this reliably exposes the version crash.
|
||||
await captureRowSignatures(ctx, 1, [1, 2, 3]);
|
||||
const pages = await floodDistinctGlyphs(ctx, 10);
|
||||
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
||||
await ctx.page.waitForTimeout(300);
|
||||
expect(errors, `renderer crashed during atlas merges: ${errors[0] ?? ''}`).toEqual([]);
|
||||
} finally {
|
||||
ctx.page.off('pageerror', onError);
|
||||
}
|
||||
});
|
||||
|
||||
test('pinned header stays correct while body churns many pages', async () => {
|
||||
const cols = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
await writeAndWaitForRender(ctx, 'HEADER_REF_0123456789');
|
||||
const reference = await captureRowSignatures(ctx, 1, cols);
|
||||
|
||||
// Body churns unique glyphs (forces merges) while header line stays on screen.
|
||||
const pages = await floodDistinctGlyphs(ctx, 8);
|
||||
expect(pages, 'should have allocated/merged pages').toBeGreaterThan(1);
|
||||
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, cols);
|
||||
for (let i = 0; i < cols.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `header glyph col ${cols[i]} corrupted by single-terminal merges`);
|
||||
}
|
||||
});
|
||||
|
||||
test('visible rows match content while scrolling (no merge)', async () => {
|
||||
// Big scrollback of distinct glyphs across multiple pages (no tiny page cap,
|
||||
// so no merge), then scroll up/down repeatedly.
|
||||
for (let c = 0; c < 8; c++) {
|
||||
await writeAndWaitForRender(ctx, generateUniqueGlyphFlood(24 * 40, 80, c * 24 * 40));
|
||||
}
|
||||
expect(await getAtlasPageCount(ctx)).toBeGreaterThan(1);
|
||||
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const top = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
||||
await ctx.proxy.scrollToBottom();
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
const topAgain = await captureRowSignatures(ctx, 1, [1, 2, 3, 4, 5]);
|
||||
for (let i = 0; i < top.length; i++) {
|
||||
expectSignatureMatches(topAgain[i], top[i], 14, `row1 col${i + 1} changed after scroll round-trip`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,152 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* WebGL "heavy TUI" stress tests: large scrollback + scrolling, alternate-buffer
|
||||
* full-screen redraws (like an agent CLI animating output), high-volume
|
||||
* rendering and replay of recorded real-world TUI sessions (vim, colored ls).
|
||||
*
|
||||
* The shared invariant: a known reference row of glyphs, captured in a fresh
|
||||
* atlas, must still render identically after the stress workload. If the WebGL
|
||||
* texture atlas is corrupted by the workload the reference glyphs change and the
|
||||
* test fails. See microsoft/vscode#322756.
|
||||
*
|
||||
* Chromium + SwiftShader only (additive scope; Firefox/WebKit WebGL are broken
|
||||
* in CI per xtermjs#5854).
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import test, { expect } from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import {
|
||||
assertTextureAtlasPresent,
|
||||
captureRowSignatures,
|
||||
expectSignatureMatches,
|
||||
loadWebglStrict,
|
||||
waitForRender,
|
||||
writeAndWaitForRender
|
||||
} from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateMixedGlyphBlock, generateSyntheticTuiFrames, generateUniqueGlyphFlood } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
const REFERENCE_COLS = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
const REFERENCE_LINE = 'Ref0123456789ABCDEF';
|
||||
|
||||
let ctx: ITestContext;
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
ctx = await createTestContext(browser);
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('WebGL heavy TUI / scrolling stress', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium', 'WebGL stress tests run on Chromium/SwiftShader only');
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await openTerminal(ctx, { cols: 80, rows: 24, scrollback: 5000 });
|
||||
await loadWebglStrict(ctx);
|
||||
});
|
||||
|
||||
test('reference glyphs survive heavy scrolling through large scrollback', async () => {
|
||||
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}\r\n`);
|
||||
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
|
||||
// Push the reference far up into scrollback with thousands of varied glyphs.
|
||||
for (let i = 0; i < 20; i++) {
|
||||
await ctx.proxy.write(generateMixedGlyphBlock(i + 1, 30, 70));
|
||||
}
|
||||
await waitForRender(ctx);
|
||||
|
||||
// Scroll around, then back to the very top where the reference lives.
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
await ctx.proxy.scrollPages(3);
|
||||
await waitForRender(ctx);
|
||||
await ctx.proxy.scrollToTop();
|
||||
await waitForRender(ctx);
|
||||
|
||||
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after heavy scrolling`);
|
||||
}
|
||||
});
|
||||
|
||||
test('alternate-buffer TUI redraws do not corrupt the restored primary buffer', async () => {
|
||||
// Reference content lives in the PRIMARY buffer.
|
||||
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}`);
|
||||
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
|
||||
// Animate a synthetic agent TUI in the alternate buffer.
|
||||
const frames = generateSyntheticTuiFrames({ seed: 7, frames: 30, cols: 80, rows: 24, useAltBuffer: true });
|
||||
for (const frame of frames) {
|
||||
await ctx.proxy.write(frame);
|
||||
}
|
||||
await waitForRender(ctx);
|
||||
await assertTextureAtlasPresent(ctx);
|
||||
|
||||
// Back in the primary buffer the reference glyphs must be intact.
|
||||
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `primary-buffer glyph at col ${REFERENCE_COLS[i]} corrupted by alt-buffer TUI redraws`);
|
||||
}
|
||||
});
|
||||
|
||||
test('reference glyphs survive a heavy mixed render + unique-glyph churn', async () => {
|
||||
// Capture the reference rendering of a known line in a fresh atlas.
|
||||
await writeAndWaitForRender(ctx, `${REFERENCE_LINE}`);
|
||||
const reference = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
|
||||
// High-volume rendering: synthetic TUI frames (which clear/redraw the screen)
|
||||
// plus a large unique-glyph flood to churn the atlas, mimicking a long agent
|
||||
// CLI session.
|
||||
const frames = generateSyntheticTuiFrames({ seed: 3, frames: 40, cols: 80, rows: 24 });
|
||||
for (const frame of frames) {
|
||||
await ctx.proxy.write(frame);
|
||||
}
|
||||
await ctx.proxy.write(generateUniqueGlyphFlood(3000, 80));
|
||||
await waitForRender(ctx);
|
||||
|
||||
// The synthetic frames erase the screen, so re-render the reference line and
|
||||
// assert the (heavily churned) atlas still draws those glyphs correctly.
|
||||
await writeAndWaitForRender(ctx, `\x1b[2J\x1b[H${REFERENCE_LINE}`);
|
||||
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], reference[i], 14, `reference glyph at col ${REFERENCE_COLS[i]} corrupted after heavy render`);
|
||||
}
|
||||
});
|
||||
|
||||
test('replaying recorded TUI sessions renders without errors or atlas corruption', async () => {
|
||||
const pageErrors: string[] = [];
|
||||
const onError = (err: Error): void => {
|
||||
pageErrors.push(err.message);
|
||||
};
|
||||
ctx.page.on('pageerror', onError);
|
||||
try {
|
||||
const fixtures = [
|
||||
'./test/fixtures/escape_sequence_files/t0090-alt_screen.in',
|
||||
'./test/fixtures/escape_sequence_files/t0502-bash_ls_color.in',
|
||||
'./test/fixtures/escape_sequence_files/t0500-bash_long_line.in',
|
||||
'./test/fixtures/escape_sequence_files/t0504-vim.in'
|
||||
];
|
||||
for (const fixture of fixtures) {
|
||||
const data = readFileSync(fixture, { encoding: 'latin1' });
|
||||
await ctx.proxy.write(data);
|
||||
}
|
||||
await waitForRender(ctx);
|
||||
await assertTextureAtlasPresent(ctx);
|
||||
|
||||
// Re-rendering the final frame must be idempotent (self-consistent): if the
|
||||
// atlas were corrupted, a forced redraw would differ from what's shown.
|
||||
const before = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
await ctx.proxy.refresh(0, (await ctx.proxy.rows) - 1);
|
||||
await waitForRender(ctx);
|
||||
const after = await captureRowSignatures(ctx, 1, REFERENCE_COLS);
|
||||
for (let i = 0; i < REFERENCE_COLS.length; i++) {
|
||||
expectSignatureMatches(after[i], before[i], 14, `fixture replay frame not self-consistent at col ${REFERENCE_COLS[i]}`);
|
||||
}
|
||||
expect(pageErrors, `no page errors during fixture replay, got: ${pageErrors.join(', ')}`).toEqual([]);
|
||||
} finally {
|
||||
ctx.page.off('pageerror', onError);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2025 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
* Produces BEFORE/AFTER screenshots of a single terminal under merge-heavy
|
||||
* churn for manual inspection (before.png / after.png, mono-*.png). On current
|
||||
* master the single-terminal merge path is healthy, so the pinned header lines
|
||||
* are expected to remain readable in the "after" images; a corrupted header in
|
||||
* these captures indicates a renderer regression. Note the mono "after" body is
|
||||
* SUPPOSED to look like glyph soup — that is the mixed box-drawing/braille
|
||||
* payload rendered faithfully, not corruption.
|
||||
*/
|
||||
import test from '@playwright/test';
|
||||
import { ITestContext, createTestContext, openTerminal } from '../../../test/playwright/TestUtils';
|
||||
import { loadWebglStrict, setMaxAtlasPages, writeAndWaitForRender } from '../../../test/playwright/RendererTestUtils';
|
||||
import { generateColoredAsciiFlood, generateMixedGlyphBlock } from '../../../test/playwright/SyntheticTui';
|
||||
|
||||
test.describe('visual #322756', () => {
|
||||
test.skip(({ browserName }) => browserName !== 'chromium');
|
||||
test.describe.configure({ timeout: 60000 });
|
||||
|
||||
test('capture before/after pinned header under merge churn', async ({ browser }) => {
|
||||
const ctx: ITestContext = await createTestContext(browser);
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
const screen = ctx.page.locator('#terminal-container .xterm-screen');
|
||||
// Header + readable sentences pinned at top; body region churns below.
|
||||
await writeAndWaitForRender(ctx, '\x1b[?1049h\x1b[H\x1b[97mHEADER: THE QUICK BROWN FOX 0123456789\r\nSTATUS: this line should stay readable\r\n');
|
||||
await screen.screenshot({ path: 'out-esbuild-test/playwright/before.png' });
|
||||
for (let c = 0; c < 30; c++) {
|
||||
await writeAndWaitForRender(ctx, '\x1b[5;1H' + generateColoredAsciiFlood(20 * 78, c * 20 * 78));
|
||||
}
|
||||
await screen.screenshot({ path: 'out-esbuild-test/playwright/after.png' });
|
||||
await ctx.page.close();
|
||||
});
|
||||
|
||||
// Issue-like framing: prefill realistic CLI text, force merges with a mixed
|
||||
// glyph payload, then rewrite the top lines clean. The rewritten header lines
|
||||
// must stay readable in mono-after.png.
|
||||
test('capture mono before/after (issue-like)', async ({ browser }) => {
|
||||
const ctx: ITestContext = await createTestContext(browser);
|
||||
await openTerminal(ctx, { cols: 80, rows: 24 });
|
||||
await loadWebglStrict(ctx);
|
||||
await setMaxAtlasPages(ctx, 4);
|
||||
const screen = ctx.page.locator('#terminal-container .xterm-screen');
|
||||
// Prefill a screenful of normal-looking CLI output (all rendered fine).
|
||||
await writeAndWaitForRender(ctx, '\x1b[?1049h\x1b[2J\x1b[H');
|
||||
let pre = '';
|
||||
for (let r = 0; r < 22; r++) {
|
||||
pre += ` $ Shell inspect bundled MCP JSON schema for type enum values ${r} lines\r\n`;
|
||||
}
|
||||
await writeAndWaitForRender(ctx, pre);
|
||||
await screen.screenshot({ path: 'out-esbuild-test/playwright/mono-before.png' });
|
||||
// Overflow the atlas (cap=4) so pages MERGE while most rows keep their cells,
|
||||
// then rewrite a few lines clean (like scroll/redraw).
|
||||
for (let c = 0; c < 12; c++) {
|
||||
let frame = '\x1b[3;1H';
|
||||
frame += generateMixedGlyphBlock(c + 1, 18, 78);
|
||||
await writeAndWaitForRender(ctx, frame);
|
||||
}
|
||||
await writeAndWaitForRender(ctx, '\x1b[1;1H\x1b[2K $ Shell grep installed Copilot CLI binary - 53 lines\r\n\x1b[2K Thought for 1s');
|
||||
await screen.screenshot({ path: 'out-esbuild-test/playwright/mono-after.png' });
|
||||
await ctx.page.close();
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -23,12 +23,12 @@ declare module '@xterm/addon-webgl' {
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when a new page is added to the texture atlas.
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when a page is removed from the texture atlas.
|
||||
* An event that is fired when the a page is removed from the texture atlas.
|
||||
*/
|
||||
public readonly onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
|
||||
+2
-5
@@ -19,17 +19,14 @@ for (const addon of addons) {
|
||||
// Demo job - This requires the others to be built so it's not included when building all
|
||||
// jobs.push(createJob('demo-client', [`--demo-client`]));
|
||||
|
||||
const codes = await Promise.all(jobs.map(job => {
|
||||
await Promise.all(jobs.map((job, i) => {
|
||||
return new Promise(r => {
|
||||
job.cp.on('exit', code => {
|
||||
log(`Finished \x1b[32m${job.name}\x1b[0m${code ? ' \x1b[31mwith errors\x1b[0m' : ''}`);
|
||||
r(code ?? 1);
|
||||
r(code);
|
||||
});
|
||||
});
|
||||
}));
|
||||
if (codes.some(code => code !== 0)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
|
||||
+11
-41
@@ -33,47 +33,17 @@ if (files.length === 0) {
|
||||
|
||||
console.log(`Linting ${files.length} changed file(s)...`);
|
||||
|
||||
const cwd = path.join(__dirname, '..');
|
||||
const isWin = process.platform === 'win32';
|
||||
|
||||
/**
|
||||
* @param {string} command
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
function run(command, args) {
|
||||
return new Promise(/** @type {(resolve: () => void, reject: (reason: Error) => void) => void} */ ((resolve, reject) => {
|
||||
const child = spawn(command, args, {
|
||||
stdio: 'inherit',
|
||||
cwd,
|
||||
shell: isWin
|
||||
});
|
||||
child.on('close', code => {
|
||||
if (code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`${command} exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
}));
|
||||
const eslintArgs = ['--max-warnings', '0', '--no-warn-ignored'];
|
||||
if (fix) {
|
||||
eslintArgs.push('--fix');
|
||||
}
|
||||
eslintArgs.push(...files);
|
||||
|
||||
async function main() {
|
||||
const oxlintArgs = ['--type-aware', '--max-warnings', '0'];
|
||||
if (fix) {
|
||||
oxlintArgs.push('--fix');
|
||||
}
|
||||
oxlintArgs.push(...files);
|
||||
|
||||
const oxlint = isWin ? 'oxlint.cmd' : 'oxlint';
|
||||
await run(oxlint, oxlintArgs);
|
||||
|
||||
const eslintArgs = ['--config', 'eslint.config.naming.mjs', '--max-warnings', '0', ...files];
|
||||
const eslint = isWin ? 'eslint.cmd' : 'eslint';
|
||||
await run(eslint, eslintArgs);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error(err.message);
|
||||
process.exit(1);
|
||||
const eslint = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
|
||||
const child = spawn(eslint, eslintArgs, {
|
||||
stdio: 'inherit',
|
||||
cwd: path.join(__dirname, '..'),
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
|
||||
child.on('close', code => process.exit(code ?? 0));
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import { basename, dirname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
||||
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
@@ -13,7 +13,7 @@ const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
/** @param {string} message */
|
||||
function log(message) {
|
||||
console.info(`[setup-full] ${message}`);
|
||||
console.info(`[setup-fast] ${message}`);
|
||||
}
|
||||
|
||||
/** @param {string[]} args */
|
||||
|
||||
+3
-12
@@ -12,7 +12,7 @@ let argv = process.argv.slice(2);
|
||||
let suiteFilter = undefined;
|
||||
while (argv.some(e => e.startsWith('--suite='))) {
|
||||
const i = argv.findIndex(e => e.startsWith('--suite='));
|
||||
const match = argv[i].match(/--suite=(?<suitename>.+)/);
|
||||
const match = argv[i].match(/--suite=(?<suitename>.+)/)
|
||||
suiteFilter = match?.groups?.suitename ?? undefined;
|
||||
argv.splice(i, 1);
|
||||
}
|
||||
@@ -42,18 +42,12 @@ if (suiteFilter) {
|
||||
configs = configs.filter(e => e.name === suiteFilter);
|
||||
}
|
||||
|
||||
if (suiteFilter && configs.length === 0) {
|
||||
console.error(`Unknown suite: ${suiteFilter}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function npmBinScript(script) {
|
||||
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
|
||||
`${script}.cmd` : script));
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let exitCode = 0;
|
||||
for (const config of configs) {
|
||||
const command = npmBinScript('playwright');
|
||||
const args = ['test', '-c', config.path, ...argv];
|
||||
@@ -68,13 +62,10 @@ async function run() {
|
||||
|
||||
if (run.error) {
|
||||
console.error(run.error);
|
||||
process.exit(run.status ?? 1);
|
||||
process.exit(run.status ?? -1);
|
||||
}
|
||||
|
||||
if (run.status) {
|
||||
exitCode = run.status;
|
||||
}
|
||||
process.exit(run.status);
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
run();
|
||||
|
||||
@@ -177,7 +177,7 @@ const ENC = {
|
||||
}
|
||||
|
||||
function printMenu() {
|
||||
console.log('\x1b[2J\x1b[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log();
|
||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||
const protocols = Object.keys(PROTOCOLS);
|
||||
|
||||
+5
-3
@@ -8,7 +8,7 @@ const path = require('path');
|
||||
|
||||
const COVERAGE_LINES_THRESHOLD = 40;
|
||||
|
||||
// Add `out-esbuild` to NODE_PATH so addon path-mapped imports (common/*, browser/*) resolve.
|
||||
// Add `out` to the NODE_PATH so absolute paths can be resolved.
|
||||
const env = { ...process.env };
|
||||
env.NODE_PATH = path.resolve(__dirname, '../out-esbuild');
|
||||
|
||||
@@ -21,7 +21,7 @@ if (process.argv.length > 2) {
|
||||
flagArgs = args.filter(e => e.startsWith('--'));
|
||||
// ability to inject particular test files via
|
||||
// npm run test [testFileA testFileB ...]
|
||||
const files = args.filter(e => !e.startsWith('--'));
|
||||
files = args.filter(e => !e.startsWith('--'));
|
||||
if (files.length) {
|
||||
testFiles = files;
|
||||
}
|
||||
@@ -39,6 +39,8 @@ if (checkCoverage) {
|
||||
...testFiles,
|
||||
...flagArgs
|
||||
];
|
||||
console.info('executable', executable);
|
||||
console.info('args', args);
|
||||
const run = cp.spawnSync(
|
||||
executable,
|
||||
args,
|
||||
@@ -49,7 +51,7 @@ if (checkCoverage) {
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
process.exit(run.status ?? -1);
|
||||
process.exit(run.status);
|
||||
}
|
||||
|
||||
const run = cp.spawnSync(
|
||||
|
||||
@@ -65,8 +65,7 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
const dtIip = document.createElement('dt');
|
||||
dtIip.textContent = 'IIP (iTerm)';
|
||||
dlIip.appendChild(dtIip);
|
||||
this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
|
||||
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
|
||||
this._addDdWithButton(dlIip, 'image-demo3', 'palette');
|
||||
container.appendChild(dlIip);
|
||||
|
||||
// Kitty demos
|
||||
@@ -142,21 +141,6 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
this._terminal.write(`\x1b]1337;File=inline=1;size=${data.length}:${btoa(sdata)}\x1b\\`);
|
||||
});
|
||||
|
||||
const iipDemoMulti = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
const data = new Uint8Array(buffer);
|
||||
let sdata = '';
|
||||
for (let i = 0; i < data.length; ++i) sdata += String.fromCharCode(data[i]);
|
||||
const encoded = btoa(sdata);
|
||||
this._terminal.write('\r\n');
|
||||
this._terminal.write(`\x1b]1337;MultipartFile=inline=1\x1b\\`);
|
||||
for (let i = 0; i < encoded.length; i += 100) {
|
||||
this._terminal.write(`\x1b]1337;FilePart=${encoded.slice(i, i + 100)}\x1b\\`);
|
||||
}
|
||||
this._terminal.write(`\x1b]1337;FileEnd\x1b\\`);
|
||||
});
|
||||
|
||||
const kittyDemo = (url: string) => () => fetch(url)
|
||||
.then(resp => resp.arrayBuffer())
|
||||
.then(buffer => {
|
||||
@@ -174,8 +158,6 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
sixelDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/testfiles/test2.sixel'));
|
||||
document.getElementById('image-demo3')!.addEventListener('click',
|
||||
iipDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
document.getElementById('image-demo4')!.addEventListener('click',
|
||||
iipDemoMulti('https://raw.githubusercontent.com/jerch/xterm-wasm-parts/master/fixtures/qoi/dice.qoi'));
|
||||
document.getElementById('image-demo-kitty1')!.addEventListener('click',
|
||||
kittyDemo('https://raw.githubusercontent.com/jerch/node-sixel/master/palette.png'));
|
||||
|
||||
@@ -194,7 +176,7 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
|
||||
// }
|
||||
// }
|
||||
|
||||
const pos = (this._terminal as any)._core._mouseCoordsService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
||||
const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
|
||||
const x = pos[0] - 1;
|
||||
const y = pos[1] - 1;
|
||||
const canvas = ev.shiftKey
|
||||
|
||||
@@ -43,7 +43,7 @@ export class GpuWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
|
||||
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
|
||||
// oxlint-disable-next-line eslint-js/no-restricted-syntax
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const dpr = window.devicePixelRatio;
|
||||
canvas.style.width = `${canvas.width / dpr}px`;
|
||||
canvas.style.height = `${canvas.height / dpr}px`;
|
||||
|
||||
@@ -11,33 +11,8 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
||||
public readonly label = 'webgl';
|
||||
|
||||
private _textureAtlasContainer!: HTMLElement;
|
||||
private _stressRunning = false;
|
||||
private _stressScrolling: HTMLInputElement | undefined;
|
||||
|
||||
public build(container: HTMLElement): void {
|
||||
const stressDiv = document.createElement('div');
|
||||
const stressStart = document.createElement('button');
|
||||
stressStart.id = 'stress-start';
|
||||
stressStart.textContent = 'atlas stress start';
|
||||
stressStart.addEventListener('click', () => this._stress());
|
||||
stressDiv.appendChild(stressStart);
|
||||
|
||||
const stressStop = document.createElement('button');
|
||||
stressStop.id = 'stress-stop';
|
||||
stressStop.textContent = 'atlas stress stop';
|
||||
stressStop.addEventListener('click', () => this._stressRunning = false);
|
||||
stressDiv.appendChild(stressStop);
|
||||
|
||||
this._stressScrolling = document.createElement('input');
|
||||
this._stressScrolling.type = 'checkbox';
|
||||
this._stressScrolling.id = 'stress-scrolling';
|
||||
stressDiv.appendChild(this._stressScrolling);
|
||||
const stressScrollingLabel = document.createElement('label');
|
||||
stressScrollingLabel.htmlFor = 'stress-scrolling';
|
||||
stressScrollingLabel.textContent = 'stress scrolling';
|
||||
stressDiv.appendChild(stressScrollingLabel);
|
||||
container.appendChild(stressDiv);
|
||||
|
||||
const zoomCheckbox = document.createElement('input');
|
||||
zoomCheckbox.type = 'checkbox';
|
||||
zoomCheckbox.id = 'texture-atlas-zoom';
|
||||
@@ -68,38 +43,9 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
|
||||
}
|
||||
|
||||
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
|
||||
// oxlint-disable-next-line eslint-js/no-restricted-syntax
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const dpr = window.devicePixelRatio;
|
||||
canvas.style.width = `${canvas.width / dpr}px`;
|
||||
canvas.style.height = `${canvas.height / dpr}px`;
|
||||
}
|
||||
|
||||
private async _stress(): Promise<void> {
|
||||
const TEXT = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
|
||||
if (this._stressRunning) {
|
||||
return;
|
||||
}
|
||||
this._stressRunning = true;
|
||||
const scrolling = !!(this._stressScrolling?.checked);
|
||||
for (let r = 0; r < 256; r += 2) {
|
||||
for (let g = 0; g < 256; g += 2) {
|
||||
let s: string[] = [];
|
||||
for (let b = 0; b < 256; b += 2) {
|
||||
if (!this._stressRunning) {
|
||||
return;
|
||||
}
|
||||
const rbg = `RGB: ${[r, g, b]}`;
|
||||
s.push(`\r\x1b[38;2;${r};${g};${b}m${rbg.padEnd(18, ' ')}${TEXT}`);
|
||||
if (s.length >= 16) {
|
||||
if (scrolling) {
|
||||
await new Promise<void>(r => this._terminal.write(s.join('\r\n') + '\r\n', r));
|
||||
} else {
|
||||
await new Promise<void>(r => this._terminal.write('\x1b[H' + s.join('\r\n'), r));
|
||||
}
|
||||
s = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-42
@@ -2,54 +2,25 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"outDir": "../out-demo/client",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@xterm/addon-attach": [
|
||||
"../../addons/addon-attach"
|
||||
],
|
||||
"@xterm/addon-clipboard": [
|
||||
"../../addons/addon-clipboard"
|
||||
],
|
||||
"@xterm/addon-fit": [
|
||||
"../../addons/addon-fit"
|
||||
],
|
||||
"@xterm/addon-image": [
|
||||
"../../addons/addon-image"
|
||||
],
|
||||
"@xterm/addon-progress": [
|
||||
"../../addons/addon-progress"
|
||||
],
|
||||
"@xterm/addon-search": [
|
||||
"../../addons/addon-search"
|
||||
],
|
||||
"@xterm/addon-serialize": [
|
||||
"../../addons/addon-serialize"
|
||||
],
|
||||
"@xterm/addon-web-fonts": [
|
||||
"../../addons/addon-web-fonts"
|
||||
],
|
||||
"@xterm/addon-web-links": [
|
||||
"../../addons/addon-web-links"
|
||||
],
|
||||
"@xterm/addon-webgl": [
|
||||
"../../addons/addon-webgl"
|
||||
],
|
||||
"@xterm/addon-unicode11": [
|
||||
"../../addons/addon-unicode11"
|
||||
],
|
||||
"@xterm/addon-unicode-graphemes": [
|
||||
"../../addons/addon-unicode-graphemes"
|
||||
],
|
||||
"@xterm/addon-ligatures": [
|
||||
"../../addons/addon-ligatures"
|
||||
],
|
||||
"@xterm/addon-attach": ["../../addons/addon-attach"],
|
||||
"@xterm/addon-clipboard": ["../../addons/addon-clipboard"],
|
||||
"@xterm/addon-fit": ["../../addons/addon-fit"],
|
||||
"@xterm/addon-image": ["../../addons/addon-image"],
|
||||
"@xterm/addon-progress": ["../../addons/addon-progress"],
|
||||
"@xterm/addon-search": ["../../addons/addon-search"],
|
||||
"@xterm/addon-serialize": ["../../addons/addon-serialize"],
|
||||
"@xterm/addon-web-fonts": ["../../addons/addon-web-fonts"],
|
||||
"@xterm/addon-web-links": ["../../addons/addon-web-links"],
|
||||
"@xterm/addon-webgl": ["../../addons/addon-webgl"],
|
||||
"@xterm/addon-unicode11": ["../../addons/addon-unicode11"],
|
||||
"@xterm/addon-unicode-graphemes": ["../../addons/addon-unicode-graphemes"],
|
||||
"@xterm/addon-ligatures": ["../../addons/addon-ligatures"],
|
||||
"*": ["./*"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -118,7 +118,7 @@ function startServer(): void {
|
||||
});
|
||||
|
||||
appWs.ws('/terminals/:pid', function (ws, req) {
|
||||
const term = terminals[parseInt(Array.isArray(req.params.pid) ? req.params.pid[0] : req.params.pid)];
|
||||
const term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
temporaryDisposable[term.pid].dispose();
|
||||
delete temporaryDisposable[term.pid];
|
||||
|
||||
+1
-2
@@ -37,7 +37,7 @@ export default tseslint.config(
|
||||
'@stylistic/comma-spacing': ['warn', { before: false, after: true }],
|
||||
'@stylistic/indent': ['warn', 2],
|
||||
'@stylistic/semi': ['warn', 'always'],
|
||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
|
||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
|
||||
'@stylistic/member-delimiter-style': ['warn', {
|
||||
multiline: { delimiter: 'semi', requireLast: true },
|
||||
singleline: { delimiter: 'comma', requireLast: false }
|
||||
@@ -86,7 +86,6 @@ export default tseslint.config(
|
||||
'eqeqeq': ['warn', 'always'],
|
||||
'jsdoc/check-alignment': 'warn',
|
||||
'jsdoc/check-param-names': 'warn',
|
||||
'jsdoc/multiline-blocks': 'warn',
|
||||
'jsdoc/no-multi-asterisks': 'warn',
|
||||
'keyword-spacing': 'warn',
|
||||
'max-len': ['warn', {
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
// @ts-check
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const ignores = [
|
||||
'addons/*/src/third-party/*.ts',
|
||||
'**/out/*',
|
||||
'**/out-test/*',
|
||||
'**/out-esbuild/*',
|
||||
'**/out-esbuild-test/*',
|
||||
'**/inwasm-sdks/*',
|
||||
'**/node_modules',
|
||||
'**/*.js',
|
||||
'**/*.mjs'
|
||||
];
|
||||
|
||||
const namingConventionMain = [
|
||||
'warn',
|
||||
{ selector: 'default', format: ['camelCase'], filter: { regex: '^[a-z]', match: true } },
|
||||
{ selector: 'variable', format: ['camelCase', 'UPPER_CASE'] },
|
||||
{ selector: 'variable', filter: '^I.+Service$', format: ['PascalCase'], prefix: ['I'] },
|
||||
{ selector: 'memberLike', modifiers: ['private'], format: ['camelCase'], leadingUnderscore: 'require' },
|
||||
{ selector: 'memberLike', modifiers: ['protected'], format: ['camelCase'], leadingUnderscore: 'require' },
|
||||
{ selector: 'enumMember', format: ['UPPER_CASE'] },
|
||||
{ selector: 'property', modifiers: ['public'], format: ['camelCase', 'UPPER_CASE'], filter: { regex: '^[a-z]', match: true } },
|
||||
{ selector: 'method', modifiers: ['public'], format: ['camelCase', 'UPPER_CASE'], custom: { regex: '^on[A-Z].+', match: false } },
|
||||
{ selector: 'method', modifiers: ['private'], format: ['camelCase'], leadingUnderscore: 'require', custom: { regex: '^on[A-Z].+', match: false } },
|
||||
{ selector: 'method', modifiers: ['protected'], format: ['camelCase'], leadingUnderscore: 'require', custom: { regex: '^on[A-Z].+', match: false } },
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
{ selector: 'interface', format: ['PascalCase'], prefix: ['I'] }
|
||||
];
|
||||
|
||||
const namingConventionTypings = [
|
||||
'warn',
|
||||
{ selector: 'typeLike', format: ['PascalCase'] },
|
||||
{ selector: 'interface', format: ['PascalCase'], prefix: ['I'] }
|
||||
];
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
ignores: ['typings/**/*.d.ts', 'addons/**/typings/**'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
projectService: true,
|
||||
tsconfigRootDir: import.meta.dirname
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': namingConventionMain
|
||||
}
|
||||
},
|
||||
{
|
||||
files: ['typings/**/*.d.ts'],
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin
|
||||
},
|
||||
languageOptions: {
|
||||
parser: tseslint.parser
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/naming-convention': namingConventionTypings
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -17,7 +17,7 @@ export default tseslint.config(
|
||||
rules: {
|
||||
'@stylistic/indent': ['warn', 2],
|
||||
'@stylistic/semi': ['warn', 'always'],
|
||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
|
||||
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
|
||||
'@stylistic/member-delimiter-style': ['warn', {
|
||||
multiline: { delimiter: 'semi', requireLast: true },
|
||||
singleline: { delimiter: 'comma', requireLast: false }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user