Compare commits

..
Author SHA1 Message Date
Daniel ImmsandCursor 3395d051a9 fix(parser): WASM scratch layout and scan cache for chunked parse
Place parser state above the linked transition table so scans emit ops
correctly, and preserve or clear the WASM scan cache across nested and
reused parse buffers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 15:31:23 -07:00
Daniel ImmsandCursor 31b8409120 fix(parser): chunked WASM scan, op coalescing, and benchmark runtime paths
The WASM scanner could fill its fixed 4096-op buffer on large inputs (e.g. 5MB
of newlines or OSC traffic) and previously failed or appeared hung. ESC op
coalescing also corrupted the lengths field used for dispatch, causing millions
of redundant handler invocations on OSC benchmark data.

WASM scanner (parser.c / parser.h):
- Add scan_offset to ParserScanHeader; scan(offset, length) resumes after a
  partial fill instead of returning -1 for the whole buffer.
- Return early from scan when the op buffer is full; all emit sites use
  emit_or_stop() so scan_offset records where to continue.
- Coalesce consecutive OP_EXECUTE and OP_ESC ops with the same code/ident;
  lengths holds repeat count, starts holds the input index for ESC.
- Keep CSI/DCS hook layout unchanged (lengths = final byte index, starts =
  param arena offset).

TypeScript dispatch (EscapeSequenceParserWasm.ts):
- Drive parsing via runWasmScanChunks(): scan a slice, dispatch ops, advance
  inputOffset to nextOffset until the chunk is consumed.
- Fix ESC dispatch: chunkPos uses starts (input index); escCount uses lengths
  (repeat count). Avoid treating the CSI-style final-byte index as a loop bound.
- Throw if nextOffset does not advance to catch stuck scans.

EscapeSequenceParser.ts:
- Extend _scanCache for chunked scan state; reset() clears inputOffset and
  cached scan data so later parses are not skipped.

Benchmarks:
- Import compiled modules from ../../out/common and ../../out/browser so
  npm run benchmark works without NODE_PATH after path-mapping removal.
- Update benchmark skill notes accordingly.

Build:
- build_parser_wasm.sh selects a clang with wasm32 support (Homebrew LLVM on
  macOS when Xcode clang lacks the target).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 15:06:00 -07:00
Cursor AgentandDaniel Imms 3eca55a80d feat(parser): WASM VT500 scanner with ScanResult SOA dispatch
- Add C WASM scanner emitting structure-of-arrays ops (kinds, starts, lengths, aux, params)
- Integrate scan+dispatch in EscapeSequenceParser with async handler resume
- Fix CSI fast-path final byte index, ERROR state reporting, and precedingJoinState for REP
- Add build pipeline (build_parser_wasm.sh, transition table generator, wasm embed)

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-31 21:35:02 +00:00
Daniel Imms 936442c813 Merge pull request #5964 from PerBothner/decoration-test
Avoid "fake markers" in DecorationService.test.ts.
2026-05-31 11:57:36 -07:00
Daniel Imms dfb65ca66f Add mise.toml to .gitignore (#5968)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Ignores local `mise.toml` configuration at the repository root so it is not committed.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-02c1c884-aae2-4267-b29b-fe7b93574ebe"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-02c1c884-aae2-4267-b29b-fe7b93574ebe"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>
2026-05-31 10:52:07 -07:00
Per Bothner 35c91cbf2d Merge branch 'master' into decoration-test 2026-05-31 10:13:17 -07:00
Cursor AgentandDaniel Imms 587856e2ca Add mise.toml to .gitignore
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-31 17:08:48 +00:00
Daniel Imms 4aa04c6b3f Merge pull request #5967 from Tyriar/migrate-eslint-to-oxlint-5690
Migrate linting from ESLint to Oxlint (Fixes #5690)
2026-05-31 09:59:34 -07:00
Per Bothner 28eadddb79 Merge branch 'master' into decoration-test 2026-05-31 09:49:20 -07:00
Daniel ImmsandCursor 59d1ec7bf0 Migrate linting from ESLint to Oxlint (Fixes #5690)
Replace the primary lint pipeline with oxlint and oxlint-tsgolint for type-aware TypeScript rules, preserving existing rule behavior from eslint.config.mjs via @oxlint/migrate and targeted jsPlugins (@stylistic/eslint-plugin, eslint-plugin-jsdoc as jsdoc-js, oxlint-plugin-eslint for no-restricted-syntax and object-curly-spacing).

Keep a hybrid ESLint pass (eslint.config.naming.mjs) only for @typescript-eslint/naming-convention until tsgolint implements it (oxc-project/tsgolint#186). Legacy eslint.config.mjs and eslint.config.typings.mjs remain for reference but are no longer used by npm scripts.

Add .oxlintrc.json for src/addons/demo/test and .oxlintrc.typings.json for typings/ (syntax-only, no --type-aware). Update lint, lint-fix, lint-api, and lint-changes to run oxlint first, then naming ESLint. Platform binaries install via oxlint-tsgolint optionalDependencies on Linux, macOS, and Windows.

Fixes https://github.com/xtermjs/xterm.js/issues/5690

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 09:48:32 -07:00
Per Bothner 7c82c1a815 Update more DecorationService tests to use existing bufferService. 2026-05-31 09:48:19 -07:00
Daniel Imms 04a0b7347c Merge pull request #5966 from Tyriar/remove-path-mappings-5896
Remove TypeScript path mappings in favor of relative imports
2026-05-31 09:31:57 -07:00
Daniel ImmsandCursor 5d8da19c44 Keep path mappings for addons after core migration
Follow-up to https://github.com/xtermjs/xterm.js/issues/5896 / PR #5966.

Converting addons to ../../../src/common relative imports broke unit tests:
esbuild emits addon code under addons/*/out-esbuild/ without mirroring the
shared tree, so Node could not load TypeScript sources at runtime
(Cannot find module '../../../src/common/Lifecycle').

Core src/, test/, and headless continue to use relative imports with no
common/* path mappings. Addons keep the prior model until build layout
can share out-esbuild the same way as the main package.

Changes:
- Restore common/* and browser/* paths in addon src/test/benchmark tsconfigs
- Revert addon imports to common/ and browser/ path-mapped specifiers
- Restore NODE_PATH=out-esbuild in bin/test_unit.js for Mocha resolution
- Restore webpack resolve.alias on addons that bundle against out/

Fixes CI test-unit failures on remove-path-mappings-5896.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 09:25:12 -07:00
Daniel ImmsandCursor 17ad33dc3d Remove TypeScript path mappings in favor of relative imports
Fixes https://github.com/xtermjs/xterm.js/issues/5896

TypeScript path mappings for common/*, browser/*, and headless/*
were not rewritten in emitted JavaScript, which forced webpack resolve
aliases and NODE_PATH workarounds so runtime could resolve modules.
That added build complexity and broke tooling such as ESLint import/no-cycle.

Changes:
- Convert ~750 imports across src/, addons/, and test/ to relative paths
- Remove common/browser/headless paths and src catch-all mappings from tsconfigs
  (keep @xterm/addon-* demo and addon typings paths)
- Remove webpack resolve.alias for common, browser, headless, and vs
- Drop NODE_PATH from unit tests, benchmarks, and VS Code launch config
- Fix addon-image IIPImageStorage to import ./Types after removing catch-all *

Verified: npm run build, esbuild, package, package-headless
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 09:16:10 -07:00
Per Bothner f76c7aa8ea Avoid "fake markers" in DecorationService.test.ts.
Instead we use an actual Marker in an actual Buffer.
2026-05-30 14:07:03 -07:00
Daniel Imms 43e8365f0e Merge pull request #5960 from xtermjs/cursor/move-fixtures-1191
Move fixtures to test/fixtures
2026-05-28 07:06:21 -07:00
Cursor AgentandDaniel Imms c7a94fc0bc Move fixtures to test/fixtures and update references
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 14:01:12 +00:00
Daniel Imms 4bb9c06b5f Merge pull request #5953 from xtermjs/cursor/alt-click-for-mouse-events-5b17
Add altClickForMouseEvents option
2026-05-28 06:35:56 -07:00
Daniel Imms 00e54532b3 Merge pull request #5959 from xtermjs/cursor/setup-dev-environment-dc0c
Add Cursor Cloud specific instructions to AGENTS.md
2026-05-27 22:13:27 -07:00
Cursor AgentandDaniel Imms e96133603b Add Cursor Cloud specific instructions to AGENTS.md
Add development environment guidance for cloud agents including:
- Demo server startup notes
- Build-before-test requirements
- Note about no external services needed
- tsgo compiler note
- Lint optimization tip

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 05:10:34 +00:00
Daniel Imms b2003861ec Merge pull request #5859 from xtermjs/iip_enhancements
IIP enhancements
2026-05-27 20:36:13 -07:00
Daniel Imms 38f77fd4ca Merge branch 'master' into iip_enhancements 2026-05-27 20:16:36 -07:00
Daniel Imms d3352cf1d5 Merge pull request #5873 from xtermjs/package_upgrades
package upgrades & linter fixes
2026-05-27 20:16:08 -07:00
Daniel Imms 0c4ec34df8 Remove unnecessary @types/lru-cache 2026-05-27 20:11:20 -07:00
Daniel Imms 743852b60c Remove unneeded comment 2026-05-27 19:58:52 -07:00
Daniel Imms 2069176528 Fix lint 2026-05-27 19:58:21 -07:00
Daniel Imms acd2472961 Merge remote-tracking branch 'upstream/master' into package_upgrades 2026-05-27 19:23:43 -07:00
Daniel Imms c2ecfe7446 Merge pull request #5895 from xtermjs/dependabot/npm_and_yarn/multi-f792d6d6d9
Bump qs and express
2026-05-27 18:26:29 -07:00
Daniel Imms 5656443163 Merge pull request #5957 from xtermjs/cursor/fix-progress-webkit-flake-5577
fix(addon-progress): stabilize flaky WebKit integration test harness
2026-05-27 17:32:25 -07:00
Cursor AgentandDaniel Imms 38174907f3 fix(addon-progress): stabilize WebKit integration test harness
Remove unnecessary unawaited setViewportSize from ProgressAddon tests,
which was the line reported in flaky CI failures. Use serial mode and
scoped hooks per Playwright shared-page guidance, and guard afterAll
teardown when setup fails.

Fixes #5956

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 00:24:06 +00:00
Daniel ImmsandCursor 1b95efa8c8 Add explicit void return type to alt cursor event listener.
Fixes explicit-function-return-type lint in AltMouseCursorController.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:54:23 -07:00
Daniel Imms 91fcb46a1a Merge pull request #5955 from xtermjs/cursor/gitignore-cursor-logs-5a86
Ignore .cursor/*.log files in git
2026-05-27 16:25:02 -07:00
Daniel ImmsandCursor ce1f5f9608 Add MouseEventCssClasses const enum for enable-mouse-events class.
Centralize the CSS class name in MouseService.ts and use it across mouse mode handling and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:23:17 -07:00
Cursor AgentandDaniel Imms 1d569f873d Ignore .cursor/*.log files in git
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 23:22:32 +00:00
Daniel ImmsandCursor bf8af4a871 Toggle enable-mouse-events cursor while alt is held for mouseEventsRequireAlt.
Extract AltMouseCursorController to bind DOM listeners only when the option and mouse protocol are active.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:20:45 -07:00
Daniel ImmsandCursor f8e9a81d0e Fix mouseEventsRequireAlt by stripping alt from PTY mouse reports.
Alt is only used locally to gate passthrough; applications like tmux ignore alt-modified SGR sequences.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-27 16:11:35 -07:00
Cursor AgentandDaniel Imms 7711d780c2 Handle mouseEventsRequireAlt option changes in mouse mode
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:59:23 +00:00
dependabot[bot] e89fbf2979 Bump qs and express
Bumps [qs](https://github.com/ljharb/qs) to 6.15.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `qs` from 6.14.2 to 6.15.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2)

Updates `express` from 4.22.1 to 4.22.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/v4.22.1...v4.22.2)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.2
  dependency-type: direct:development
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 21:53:01 +00:00
Daniel Imms db244ea5a8 Merge pull request #5954 from xtermjs/cursor/align-headless-marker-alt-buffer-2866
Allow markers on alt buffer in headless terminal
2026-05-27 14:51:35 -07:00
Cursor AgentandDaniel Imms fdcd1c2720 Align xterm-headless marker JSDoc with xterm.d.ts
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:46:20 +00:00
Cursor AgentandDaniel Imms f8cef535c7 Rename altClickForMouseEvents to mouseEventsRequireAlt
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:41:44 +00:00
Cursor AgentandDaniel Imms 57b48f6048 Allow markers on alt buffer in headless terminal
Remove the alt-buffer guard in headless registerMarker so behavior
matches the browser terminal. Update headless public API and typings
to return IMarker instead of IMarker | undefined.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:28:36 +00:00
Cursor AgentandDaniel Imms 5ad56446b8 Explicitly enable selection when altClickForMouseEvents is on at attach
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:20:17 +00:00
Daniel Imms ec0c93fd5a Merge pull request #5950 from xtermjs/cursor/writebuffer-dispose-timeout-124c
Cancel WriteBuffer innerWrite timers on dispose
2026-05-27 14:14:41 -07:00
Daniel Imms be339bf796 Merge pull request #5947 from xtermjs/cursor/decoration-bucket-merge-loop-7132
Avoid spread when merging decoration line buckets
2026-05-27 13:15:37 -07:00
Cursor AgentandDaniel Imms fbb2a9710f Add altClickForMouseEvents option for issue #5952
When enabled during mouse events mode, click/drag/move events require the alt
key while wheel events are unaffected. Selection stays enabled by default
and takes precedence over macOptionClickForcesSelection.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 20:13:31 +00:00
Daniel Imms 7294124736 Merge pull request #5945 from xtermjs/cursor/bufferline-stringbuilder-5939-79f8
Use StringBuilder in BufferLine.translateToString
2026-05-27 13:11:02 -07:00
Daniel Imms 2c3fad39af Merge pull request #5948 from xtermjs/cursor/hoist-get-current-link-id-e29d
Hoist _getCurrentLinkId() in print() loop
2026-05-27 13:09:09 -07:00
Cursor AgentandDaniel Imms edb4f89dd8 Add WriteBuffer regression tests for dispose and hot paths
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 20:00:06 +00:00
Cursor AgentandDaniel Imms 224c113b64 Move linkId declaration above use site in print loop
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:36:45 +00:00
Cursor AgentandDaniel Imms 3e0c7d18e5 Register WriteBuffer cleanup via toDisposable in constructor
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:35:58 +00:00
Daniel Imms 95d9224b8c Merge pull request #5946 from xtermjs/cursor/fix-duplicate-buildnumber-guard-cbb8
Fix duplicate buildNumber guard in Windows ConPTY logic
2026-05-27 12:34:08 -07:00
Cursor AgentandDaniel Imms 7499a2181d Use DisposableStore.isDisposed instead of duplicate flag
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:34:08 +00:00
Daniel Imms 7dc820f5fa Merge pull request #5949 from xtermjs/cursor/rename-headless-register-marker-22c3
Rename headless core addMarker → registerMarker
2026-05-27 12:33:02 -07:00
Cursor AgentandDaniel Imms e59a6c1f7b Reuse shared StringBuilder in BufferLine.translateToString
Avoid per-call allocation when translating many lines in sequence by
resetting a module-level work StringBuilder before and after each use.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:31:00 +00:00
Daniel Imms f3e2c08a5d Merge pull request #5944 from xtermjs/cursor/buffer-reflow-single-loop-sum-705e
perf: use single loop for cellsNeeded sum in BufferReflow
2026-05-27 12:28:08 -07:00
Daniel Imms b138bf38d0 Merge pull request #5943 from xtermjs/cursor/remove-duplicate-defineproperty-57b2
Remove duplicate Object.defineProperty loop in headless Terminal
2026-05-27 12:26:36 -07:00
Cursor AgentandDaniel Imms c6e4627b45 Hoist _getCurrentLinkId() in print() loop
Avoid calling _getCurrentLinkId() twice per codepoint when a hyperlink
is active by reading the link id once before the print loop.

Fixes #5940

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:24 +00:00
Cursor AgentandDaniel Imms d91045dc5f Cancel WriteBuffer innerWrite timers on dispose
Guard _innerWrite and public write methods after disposal so pending
setTimeout continuations cannot run against a disposed terminal.

Fixes #5936

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:22 +00:00
Cursor AgentandDaniel Imms 41dfdba2cf Avoid spread when merging decoration line buckets
Replace existing.push(...bucket) with an indexed loop in
_mergeLineBucket to avoid intermediate array allocations during
buffer trim/insert/delete re-indexing.

Reuse _mergeLineBucket in _handleBufferLinesTrim for consistency.

Fixes #5941

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:13 +00:00
Cursor AgentandDaniel Imms ba33decb68 Rename headless core addMarker to registerMarker
Align headless Terminal core API with browser CoreBrowserTerminal
and the public registerMarker naming.

Fixes #5938

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:01 +00:00
Cursor AgentandDaniel Imms 17e3b84bcc perf: use single loop for cellsNeeded sum in BufferReflow
Replace map().reduce() with one accumulator loop in
reflowSmallerGetNewLineLengths to avoid allocating an intermediate
array when summing wrapped line trimmed lengths.

Fixes #5942

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:17:17 +00:00
Cursor AgentandDaniel Imms 182ff2f635 Remove duplicate Object.defineProperty loop in headless Terminal
The constructor defined property getters/setters twice on _publicOptions;
the first block was immediately overwritten by the second. Align with the
browser public Terminal implementation.

Fixes #5935

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:17:08 +00:00
Cursor AgentandDaniel Imms bcb4e5298b Fix duplicate buildNumber guard in Windows ConPTY logic
The windowsPty option change handler checked buildNumber twice
instead of requiring both backend and buildNumber to be defined.
This aligns with Buffer.ts and ensures ConPTY wrapping heuristics
are evaluated only when both fields are present.

Fixes #5934

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:16:54 +00:00
Cursor AgentandDaniel Imms 42facac2d9 Use StringBuilder in BufferLine.translateToString
Replace O(n²) string concatenation in the translate loop with
StringBuilder.append, matching the pattern used elsewhere in the project.

Fixes #5939

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:16:51 +00:00
Daniel Imms 2ab0f10a26 Merge pull request #5929 from xtermjs/cursor/atlas-cache-texture-size-7cad
fix(webgl): include texture size and cell dimensions in atlas cache equality
2026-05-27 10:31:32 -07:00
Daniel Imms 280dc0a36a Merge pull request #5927 from xtermjs/cursor/webgl-vao-dispose-ef2f
fix(addon-webgl): delete VAOs on renderer dispose
2026-05-27 09:34:46 -07:00
Daniel Imms ebe8cb749a Merge pull request #5928 from xtermjs/cursor/fix-dcs-osc-apc-payload-o-n-f60b
Fix O(n²) payload accumulation in DCS/OSC/APC handlers
2026-05-27 09:32:42 -07:00
Daniel Imms c28f3c8d73 Merge pull request #5922 from xtermjs/cursor/remove-stale-sgr-fixme-e362
Remove stale FIXME comments from charAttributes JSDoc
2026-05-27 09:28:29 -07:00
Daniel Imms 3bd7ac083a Merge pull request #5923 from xtermjs/cursor/texture-atlas-remove-emitter-leak-75ac
Fix TextureAtlas remove-emitter leak on dispose
2026-05-27 09:27:51 -07:00
Cursor AgentandDaniel Imms 30b42838fd Extract StringBuilder and LimitedStringBuilder with unit tests
Replace PayloadStringBuffer with LimitedStringBuilder from common.
Add thorough StringBuilder and LimitedStringBuilder test coverage.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 16:27:26 +00:00
Daniel Imms 80e832e417 Merge pull request #5930 from xtermjs/cursor/webgl-ilogservice-5921-3745
fix(addon-webgl): use ILogService instead of console for logging
2026-05-27 09:21:12 -07:00
Daniel Imms 470947abd8 Merge pull request #5931 from xtermjs/cursor/headless-clear-markers-ccc0
fix(headless): clear markers when clear() is called
2026-05-27 09:18:17 -07:00
Daniel Imms bb49a80ace Merge branch 'master' into cursor/headless-clear-markers-ccc0 2026-05-27 09:13:38 -07:00
Daniel Imms 65b87e0f40 Merge pull request #5933 from xtermjs/cursor/replace-clone-with-structuredclone-34fc
Replace custom clone utility with structuredClone
2026-05-27 09:12:41 -07:00
Daniel Imms 39fc0f259e Merge pull request #5932 from xtermjs/cursor/webgl-context-loss-timeout-dispose-3f04
Clear WebGL context-loss timeout on WebglRenderer dispose
2026-05-27 09:10:07 -07:00
Daniel Imms b787ac8b1b Merge pull request #5924 from xtermjs/cursor/selection-trim-listener-leak-135c
Fix SelectionService trim listener leak
2026-05-27 09:10:04 -07:00
Daniel Imms 457efbf55d Merge pull request #5925 from xtermjs/cursor/fix-onwillopen-error-logging-7e14
Log onWillOpen handler errors instead of swallowing them
2026-05-27 09:00:33 -07:00
Daniel Imms 42270a0b68 Merge pull request #5926 from xtermjs/cursor/fix-flushsync-onwriteparsed-7a21
Fix flushSync() to fire onWriteParsed
2026-05-27 09:00:13 -07:00
Cursor AgentandDaniel Imms 090b77f354 Replace custom clone utility with structuredClone
Remove common/Clone.ts and its tests. Use the native structuredClone API
for cloning default modes, options, and test fixtures.

Fixes #5911

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:41:00 +00:00
Cursor AgentandDaniel Imms 534ce1365b Clear WebGL context-loss timeout on WebglRenderer dispose
Fixes #5919

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:40:10 +00:00
Cursor AgentandDaniel Imms f757c965ee fix(addon-webgl): keep VAO WebGL types non-nullable
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:38:58 +00:00
Cursor AgentandDaniel Imms 4aa02f3fd9 fix(addon-webgl): route logging through ILogService instead of console
Replace direct console.log/warn/error calls in the WebGL addon with
ILogService so messages respect the terminal logLevel and custom logger.

Fixes xtermjs/xterm.js#5921

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:37:09 +00:00
Cursor AgentandDaniel Imms f43273b3f0 fix(webgl): include texture size and cell dimensions in atlas cache equality
configEquals omitted deviceMaxTextureSize, deviceCellWidth, and
deviceCellHeight, which could cause the char atlas cache to reuse an
atlas built for a different GPU texture limit or cell size.

Fixes #5915

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:35:05 +00:00
Cursor AgentandDaniel Imms c06c3025f2 fix(addon-webgl): delete VAOs on renderer dispose
Register vertex array objects for disposal in GlyphRenderer and
RectangleRenderer, matching existing buffer and program cleanup.

Fixes #5918

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:35:04 +00:00
Daniel Imms 1064e4f783 Merge pull request #5812 from JeffreyCA/osc8-hover-fix
Underline entire wrapped OSC 8 links on hover
2026-05-27 08:34:50 -07:00
Cursor AgentandDaniel Imms fad4821fd9 Fix O(n²) payload accumulation in DCS/OSC/APC handlers
Replace repeated string concatenation with a chunk buffer that joins
once on completion. Fixes #5916.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:34:48 +00:00
Cursor AgentandDaniel Imms 306c3d73ec Fix flushSync() to fire onWriteParsed
flushSync() processes pending writes synchronously (e.g. during
CoreTerminal.resize()) but did not emit onWriteParsed, unlike the
async _innerWrite path. Addons listening for parse completion could
miss updates after resize.

Fixes #5912

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:34:26 +00:00
Cursor AgentandDaniel Imms 8f2bb32c9c fix(headless): clear markers when clear() is called
Headless clear() now calls buffer.clearAllMarkers() to match the browser
implementation, preventing markers from surviving clear() and pointing at
wrong lines.

Fixes #5910

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:34:00 +00:00
Cursor AgentandDaniel Imms 7b6c814566 Log errors from onWillOpen handlers instead of swallowing them
Fixes #5920

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:33:48 +00:00
Cursor AgentandDaniel Imms adc9dea083 Fix TextureAtlas remove-emitter leak on dispose
Dispose _onRemoveTextureAtlasCanvas in TextureAtlas.dispose() alongside
_onAddTextureAtlasCanvas to avoid leaking listeners when the atlas is torn down.

Fixes #5917

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:33:28 +00:00
Cursor AgentandDaniel Imms d565a2750e Fix SelectionService trim listener leak with MutableDisposable
Register _trimListener via _register(MutableDisposable) so it is disposed
when SelectionService is torn down. Use .value assignment on buffer
activate instead of manual dispose/swap.

Fixes #5914

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:33:20 +00:00
Cursor AgentandDaniel Imms c5ab83d697 Remove stale FIXME comments from charAttributes JSDoc
Fixes #5913. Blinking is supported in renderers via TextBlinkStateManager,
and bright background colors (100-107) are handled in the SGR loop.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 15:33:03 +00:00
c62e6dd765 Register BufferNamespaceApi onBufferActivate listener with disposables (#5906)
Wrap the onBufferActivate subscription in this._register so it is
disposed when BufferNamespaceApi is disposed, matching the pattern used
elsewhere in the codebase.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 04:39:27 +00:00
Daniel Imms 29a07f1d8f Merge pull request #5905 from xtermjs/cursor/multiline-comment-style-5899-d349
Add multiline-comment-style lint rule
2026-05-26 21:05:48 -07:00
Cursor AgentandDaniel Imms 88cef97049 Attach method signatures to last handler JSDoc line in Types.ts
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 04:01:00 +00:00
Cursor AgentandDaniel Imms 0e823004e6 Use single-line JSDoc comments per handler in Types.ts
Document multi-variant escape handlers with separate /** ... */ lines
instead of multiline blocks.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 03:59:42 +00:00
Cursor AgentandDaniel Imms df3422fb0b Use jsdoc/multiline-blocks instead of multiline-comment-style
Replace multiline-comment-style (starred-block), which also forces
multiline // comments into block comments, with jsdoc/multiline-blocks.
That rule only enforces starred JSDoc layout for /** blocks (no text on
the opening line) and leaves multiline // comments unchanged for
internal documentation.

Fix multiline JSDoc in Types.ts.

Fixes #5899

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-26 20:32:23 +00:00
Cursor AgentandDaniel Imms 4ec85e67ba Add multiline-comment-style lint rule and fix violations
Enable multiline-comment-style with starred-block to require JSDoc-style
block comments with content on separate lines from the opening delimiter.
Auto-fix existing violations across the codebase.

Fixes #5899

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-26 20:15:55 +00:00
Jörg Breitbart 5a6427e867 Merge branch 'master' into package_upgrades 2026-05-21 13:45:03 +02:00
Jörg Breitbart 8519ac6d99 Merge branch 'master' into package_upgrades 2026-05-21 13:34:12 +02:00
jerch ae611694de Merge branch 'master' into iip_enhancements 2026-05-21 12:47:09 +02:00
jerch 772df0cb98 Merge branch 'master' into iip_enhancements 2026-05-11 17:03:53 +02:00
jerch aa93189cc5 Merge branch 'master' into package_upgrades 2026-05-11 16:57:49 +02:00
Jörg Breitbart f82a8029d2 fix README 2026-05-10 21:27:52 +02:00
Jörg Breitbart a2f321c887 add multipart demo 2026-05-10 21:08:22 +02:00
Jörg Breitbart eb58cefa8e add integration tests for filepart 2026-05-10 20:34:43 +02:00
Jörg Breitbart d92ca87cbe add qoi metrics test 2026-05-10 19:32:47 +02:00
jerch 224167bbbe Merge branch 'master' into iip_enhancements 2026-05-09 22:59:22 +02:00
Jörg Breitbart 740fa44ae4 make webfont loading tests more resilient 2026-05-09 18:13:16 +02:00
Jörg Breitbart 636baa8d79 make webfont loading tests more resilient 2026-05-09 18:06:59 +02:00
Jörg Breitbart 44a27dc958 make webfont loading tests more resilient 2026-05-09 17:59:33 +02:00
Jörg Breitbart dc1ae4514b package upgrades & linter fixes 2026-05-09 17:42:49 +02:00
Jörg Breitbart 494cf1f2e1 make linter happy 2026-04-30 16:54:54 +02:00
Jörg Breitbart 645865efec fix cursor placement in IIP 2026-04-30 16:46:22 +02:00
Jörg Breitbart 83e3657a82 fix _mouseCoordsService access in demo 2026-04-30 15:21:41 +02:00
Jörg Breitbart 53554a05f7 use _coreBrowserService.dpr 2026-04-30 10:44:38 +02:00
Jörg Breitbart 51c8d8abf8 implement ReportCellSize 2026-04-30 10:36:41 +02:00
Jörg Breitbart 05ea5ac324 implement ReportCellSize 2026-04-30 10:33:34 +02:00
Jörg Breitbart 9e27c7debb filepart handling in IIP 2026-04-29 21:36:45 +02:00
Jeffrey Chen 253d1e9beb Underline entire wrapped OSC 8 link on hover 2026-04-12 20:06:43 -07:00
400 changed files with 7903 additions and 3959 deletions
+1 -1
View File
@@ -25,4 +25,4 @@ When writing benchmark instructions:
## Notes
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
- Keep `NODE_PATH=./out` (handled by the npm script).
- Benchmark sources import compiled modules from `../../out/common/` and `../../out/browser/` (run `npm run build` first).
+2
View File
@@ -17,6 +17,7 @@ docs/
npm-debug.log
/.idea/
.env
mise.toml
build/
.DS_Store
yarn.lock
@@ -24,6 +25,7 @@ test-results/
# Tracing/debug files
.cursor/nal-trace/
.cursor/*.log
# Keep bundled code out of Git
dist/
+372
View File
@@ -0,0 +1,372 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"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"
},
"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"
}
}
]
}
+120
View File
@@ -0,0 +1,120 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"options": {
"maxWarnings": 0
},
"env": {
"builtin": true
},
"overrides": [
{
"files": [
"typings/**/*.d.ts"
],
"plugins": [
"typescript"
],
"jsPlugins": [
"@stylistic/eslint-plugin",
{
"name": "jsdoc-js",
"specifier": "eslint-plugin-jsdoc"
},
"oxlint-plugin-eslint"
],
"rules": {
"@stylistic/indent": [
"warn",
2
],
"@stylistic/semi": [
"warn",
"always"
],
"@stylistic/quotes": [
"warn",
"single",
{
"allowTemplateLiterals": "always"
}
],
"@stylistic/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@stylistic/type-annotation-spacing": "warn",
"@stylistic/keyword-spacing": "warn",
"@stylistic/max-len": [
"warn",
{
"code": 1000,
"comments": 80,
"ignoreUrls": true,
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
}
],
"@stylistic/no-extra-semi": "error",
"@stylistic/no-trailing-spaces": "warn",
"eslint-js/object-curly-spacing": [
"warn",
"always"
],
"@stylistic/spaced-comment": [
"warn",
"always",
{
"markers": [
"/"
],
"exceptions": [
"-"
]
}
],
"@stylistic/comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"@stylistic/eol-last": "warn",
"curly": [
"warn",
"multi-line"
],
"eqeqeq": [
"warn",
"always"
],
"jsdoc-js/check-alignment": "warn",
"jsdoc-js/check-param-names": "warn",
"no-irregular-whitespace": "warn",
"typescript/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"typescript/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"typescript/prefer-namespace-keyword": "warn"
}
}
]
}
-3
View File
@@ -15,9 +15,6 @@
"--recursive",
"${workspaceRoot}/out/**/*.test.js"
],
"env": {
"NODE_PATH": "${workspaceRoot}/out"
},
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
+13
View File
@@ -26,6 +26,7 @@ npm run build && npm run esbuild # Build all TypeScript and bundle
- Integration tests: `npm run test-integration` (Playwright across Chrome/Firefox/WebKit)
- Integration tests by file: `npm run test-integration -- test/playwright/InputHandler.test.ts`. Never use grep to filter tests, it doesn't work
- Integration tests by addon: `npm run test-integration -- --suite=addon-search`. Suites always follow the format `addon-<something>`
- Lint: `npm run lint` (oxlint with type-aware rules, then ESLint for `naming-convention` only), `npm run lint-api` for `typings/`, `npm run lint-fix` for oxlint auto-fix
- Lint changes: `npm run lint-changes` to lint only changed files, `npm run lint-changes-fix` to fix them
## Addon Development Pattern
@@ -98,3 +99,15 @@ 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.
+20 -31
View File
@@ -1,6 +1,7 @@
## @xterm/addon-image
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).
Inline image output in xterm.js. Supports SIXEL, iTerm's inline image protocol (IIP)
and partially Kitty's terminal graphics prtocol (TGP).
![](fixture/example.png)
@@ -43,9 +44,6 @@ 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)
@@ -73,7 +71,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.
Other cursor positioning modes as used by xterm or mintty are not supported for sixel.
- **SIXEL Palette Handling**
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
@@ -100,17 +98,24 @@ 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 and GIF
- Supported formats: PNG, JPEG, GIF and QOI
- 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).
- Size parameter is restricted by `iipSizeLimit`.
- Cursor positioning behaves the same as for sixel (see above).
- 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.
### Storage and Drawing Settings
@@ -127,6 +132,7 @@ 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:
@@ -140,6 +146,7 @@ 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, lets look at the data flow and processing steps:
@@ -180,8 +187,8 @@ _How can I adjust the memory usage?_
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 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.
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
Constructor settings. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
### Terminal Interaction
@@ -211,23 +218,5 @@ _How can I adjust the memory usage?_
### Status
- 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
- 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.
Binary file not shown.
+70 -16
View File
@@ -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 } from './IIPHeaderParser';
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
// Local const enum mirror - esbuild can't inline const enums from external packages
@@ -23,6 +23,7 @@ const enum DecoderConst {
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
type: SequenceType.INVALID,
name: 'Unnamed file',
size: 0,
width: 'auto',
@@ -39,6 +40,8 @@ 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,
@@ -52,11 +55,14 @@ export class IIPHandler implements IOscHandler, IResetHandler {
this._qoiDec = new QoiDecoder(DecoderConst.KEEP_DATA);
}
public reset(): void {}
public reset(): void {
this._hp.reset();
this._dec.release();
this._qoiDec.release();
}
public start(): void {
this._aborted = false;
this._header = DEFAULT_HEADER;
this._metrics = UNSUPPORTED_TYPE;
this._hp.reset();
}
@@ -76,15 +82,27 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return;
}
if (dataPos > 0) {
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
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._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;
}
}
}
@@ -93,22 +111,58 @@ 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 = true;
let cond: number | boolean;
if (cond = success) {
if (cond = !this._dec.end()) {
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;
}
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;
}
}
}
+129 -7
View File
@@ -4,19 +4,20 @@
*/
import { assert } from 'chai';
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
import { HeaderParser, HeaderState, IHeaderFields, SequenceType } from './IIPHeaderParser';
const CASES: [string, IHeaderFields][] = [
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
['File=size=123456;name=dGVzdA==:', {type: SequenceType.FILE, name: 'test', size: 123456}],
['File=size=123456;name=dGVzdA:', {type: SequenceType.FILE, name: 'test', size: 123456}],
// utf-8 encoding in name
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
['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}],
// 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%',
@@ -28,6 +29,7 @@ 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',
@@ -101,7 +103,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, {name: 'test', size: 0});
assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: 'test', size: 0});
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
@@ -115,7 +117,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, {name: '', size: 123456});
assert.deepEqual(hp.fields, {type: SequenceType.FILE, name: '', size: 123456});
hp.reset();
inp = fromBs(CASES[0][0]);
res = hp.parse(inp, 0, inp.length);
@@ -135,4 +137,124 @@ 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 });
});
});
+86 -11
View File
@@ -6,9 +6,27 @@
// 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.
@@ -29,14 +47,6 @@ 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
@@ -92,7 +102,19 @@ 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;
@@ -111,12 +133,43 @@ 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 > 6) return -1;
if (state === HeaderState.START && pos > 14) return -1;
for (let i = start; i < end; ++i) {
const c = data[i];
switch (c) {
@@ -127,8 +180,29 @@ export class HeaderParser {
break;
case 61: // =
if (state === HeaderState.START) {
for (let k = 0; k < FILE_MARKER.length; ++k) {
if (buffer[k] !== FILE_MARKER[k]) return this._a();
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();
}
state = HeaderState.KEY;
pos = 0;
@@ -158,6 +232,7 @@ export class HeaderParser {
}
private _a(): number {
this.fields.type = SequenceType.INVALID;
this.state = HeaderState.ABORT;
return -1;
}
+3 -1
View File
@@ -3,6 +3,7 @@
* @license MIT
*/
import { IAddImageOpts } from './Types';
import { ImageStorage } from './ImageStorage';
/**
@@ -12,6 +13,7 @@ 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
) {}
@@ -21,6 +23,6 @@ export class IIPImageStorage {
* Always uses scrolling mode — cursor advances past the image.
*/
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._storage.addImage(img, true);
this._storage.addImage(img, this._addImageOpts);
}
}
+2 -1
View File
@@ -30,7 +30,8 @@ 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 }]
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }],
['dice.qoi', { mime: 'image/qoi', width: 800, height: 600 }],
];
+1 -6
View File
@@ -137,11 +137,6 @@ 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;
@@ -260,7 +255,7 @@ export class ImageAddon implements ITerminalAddon, IImageApi {
}
private _report(s: string): void {
this._terminal?._core.coreService.triggerDataEvent(s);
this._terminal?._core.input(s, false);
}
private _decset(params: (number | number[])[]): boolean {
+1 -1
View File
@@ -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 - start) * (this.dimensions?.css.cell.height || 0);
const h = (end + 1 - start) * (this.dimensions?.css.cell.height || 0);
if (!layer || layer === 'top') {
this._layers.get('top')?.clearRect(0, y, w, h);
}
+22 -13
View File
@@ -5,7 +5,11 @@
import { IDisposable } from '@xterm/xterm';
import { ImageRenderer } from './ImageRenderer';
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle, ImageLayer } from './Types';
import {
ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec,
IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes,
UnderlineStyle, IAddImageOpts
} from './Types';
// fallback default cell size
@@ -235,13 +239,15 @@ export class ImageStorage implements IDisposable {
/**
* Method to add an image to the storage.
* @param img - The image to add (canvas or bitmap).
* @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.
* @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.
* @returns The internal image ID assigned to the stored image.
*/
public addImage(img: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer = 'top', zIndex: number = 0): number {
public addImage(img: HTMLCanvasElement | ImageBitmap, opts: IAddImageOpts): number {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
@@ -263,7 +269,7 @@ export class ImageStorage implements IDisposable {
let offset = originX;
let tileCount = 0;
if (!scrolling) {
if (!opts.scrolling) {
buffer.x = 0;
buffer.y = 0;
offset = 0;
@@ -277,7 +283,7 @@ export class ImageStorage implements IDisposable {
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
tileCount++;
}
if (scrolling) {
if (opts.scrolling) {
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
} else {
if (++buffer.y >= termRows) break;
@@ -287,8 +293,12 @@ export class ImageStorage implements IDisposable {
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
// cursor positioning modes
if (scrolling) {
buffer.x = offset;
if (opts.scrolling) {
if (opts.cursorPos === 'iip') {
buffer.x = Math.min(offset + cols, termCols);
} else {
buffer.x = offset;
}
} else {
buffer.x = originX;
buffer.y = originY;
@@ -331,8 +341,8 @@ export class ImageStorage implements IDisposable {
marker: endMarker || undefined,
tileCount,
bufferType: this._terminal.buffer.active.type,
layer,
zIndex
layer: opts.layer,
zIndex: opts.zIndex
};
// finally add the image
@@ -455,7 +465,6 @@ export class ImageStorage implements IDisposable {
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break;
}
e = nextE;
count++;
}
col--;
+4 -2
View File
@@ -4,7 +4,7 @@
*/
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { IImageAddonOptions, ITerminalExt } from './Types';
import { IImageAddonOptions, ITerminalExt, IAddImageOpts } from './Types';
import { ImageRenderer } from './ImageRenderer';
/**
@@ -15,6 +15,7 @@ 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,
@@ -27,7 +28,8 @@ export class SixelImageStorage {
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
*/
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
this._storage.addImage(img, this._opts.sixelScrolling);
this._addImageOpts.scrolling = this._opts.sixelScrolling;
this._storage.addImage(img, this._addImageOpts);
}
/**
+8
View File
@@ -100,6 +100,14 @@ 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;
@@ -5,7 +5,7 @@
import { IDisposable } from '@xterm/xterm';
import { ImageStorage } from '../ImageStorage';
import { ImageLayer } from '../Types';
import { ImageLayer, IAddImageOpts } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes';
// Kitty-specific image storage controller.
@@ -42,6 +42,7 @@ 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
@@ -98,7 +99,10 @@ export class KittyImageStorage implements IDisposable {
if (oldStorageId !== undefined) {
this._storageIdToKittyId.delete(oldStorageId);
}
const storageId = this._storage.addImage(image, scrolling, layer, zIndex);
this._addImageOpts.scrolling = scrolling;
this._addImageOpts.layer = layer;
this._addImageOpts.zIndex = zIndex;
const storageId = this._storage.addImage(image, this._addImageOpts);
this._kittyIdToStorageId.set(kittyId, storageId);
this._storageIdToKittyId.set(storageId, kittyId);
}
@@ -290,6 +290,67 @@ 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]);
+6 -12
View File
@@ -539,7 +539,6 @@ 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; });
@@ -548,12 +547,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=31,a=q;\x1b\\');
await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse');
const 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; });
@@ -562,7 +560,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);
response = await ctx.page.evaluate('window.kittyResponse');
const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=42;OK\x1b\\');
});
@@ -578,7 +576,6 @@ 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; });
@@ -587,12 +584,11 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=60,a=q,f=100;!!!invalid!!!\x1b\\');
await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse');
const response: string = 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; });
@@ -601,7 +597,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=70,a=q,f=24;AAAA\x1b\\');
await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse');
const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=70;EINVAL:width and height required for raw pixel data\x1b\\');
});
@@ -642,7 +638,6 @@ 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; });
@@ -652,12 +647,11 @@ 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);
response = await ctx.page.evaluate('window.kittyResponse');
const 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; });
@@ -667,7 +661,7 @@ test.describe('Kitty Graphics Protocol', () => {
await ctx.proxy.write('\x1b_Gi=101,I=201,a=d\x1b\\');
await timeout(100);
response = await ctx.page.evaluate('window.kittyResponse');
const response = await ctx.page.evaluate('window.kittyResponse');
strictEqual(response, '\x1b_Gi=101;EINVAL:cannot specify both i and I keys\x1b\\');
});
});
+4 -8
View File
@@ -31,15 +31,11 @@
],
"license": "MIT",
"dependencies": {
"lru-cache": "^6.0.0",
"opentype.js": "^0.8.0"
"lru-cache": "^11.3.6",
"opentype.js": "^2.0.0"
},
"devDependencies": {
"@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"
"@types/opentype.js": "^1.3.9",
"font-finder": "^1.1.0"
}
}
@@ -1,5 +1,5 @@
import * as opentype from 'opentype.js';
import LRUCache = require('lru-cache');
import { LRUCache } from '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({
max: options.cacheSize,
length: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
maxSize: options.cacheSize,
sizeCalculation: ((val: ILigatureData | [number, number][], key: string) => key.length) as any
});
}
@@ -44,7 +44,6 @@ 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
@@ -9,15 +9,18 @@ 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 = [];
+3 -3
View File
@@ -174,7 +174,7 @@ export class SearchEngine {
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const 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 = startCol = prevSelectedPos.start.x;
searchPosition.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 = startCol = prevSelectedPos.end.x;
searchPosition.startCol = prevSelectedPos.end.x;
}
}
}
@@ -7,10 +7,10 @@
"rootDir": "..",
"outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"],
"moduleResolution": "node",
"moduleResolution": "nodenext",
"strict": false,
"target": "es2021",
"module": "commonjs",
"module": "nodenext",
"paths": {
"common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"],
+3 -3
View File
@@ -21,8 +21,8 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start",
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
"benchmark": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
"benchmark-baseline": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
}
}
@@ -4,10 +4,10 @@
"rootDir": "..",
"outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"],
"moduleResolution": "node",
"moduleResolution": "nodenext",
"strict": false,
"target": "es2015",
"module": "commonjs",
"module": "nodenext",
"paths": {
"common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"],
+3 -3
View File
@@ -21,8 +21,8 @@
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start",
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
"benchmark": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json out-benchmark/benchmark/*benchmark.js",
"benchmark-baseline": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
"benchmark-eval": "../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
}
}
@@ -78,7 +78,22 @@ 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);
`);
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
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' }]);
}
// broken case: webfont in ctor without addon usage
await ctx.page.evaluate(`
@@ -86,15 +101,20 @@ test.describe('WebFontsAddon', () => {
window.helperTerm.open(term.element);
`);
// safari loads the font, firefox & chrome dont
fonts = await getDocumentFonts();
// safari might load the font, firefox & chrome dont
if (browser.browserType().name() === 'webkit') {
try {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loading' }]);
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loading' }]);
} catch (e) {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'loaded' }]);
try {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'loaded' }]);
} catch (e) {
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
}
}
} else {
deepStrictEqual(await getDocumentFonts(), [{ family: 'Kongtext', status: 'unloaded' }]);
deepStrictEqual(fonts, [{ family: 'Kongtext', status: 'unloaded' }]);
}
// good case: addon fixes layout for webfont in ctor
@@ -5,7 +5,6 @@
"lib": [
"es2021",
],
// "downlevelIteration": true,
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
@@ -113,8 +113,8 @@ export class LinkComputer {
let line: IBufferLine | undefined;
let topIdx = lineIndex;
let bottomIdx = lineIndex;
let length = 0;
let content = '';
let length;
let content;
const lines: string[] = [];
if ((line = terminal.buffer.active.getLine(lineIndex))) {
@@ -5,7 +5,6 @@
"lib": [
"es2021",
],
// "downlevelIteration": true,
"rootDir": ".",
"outDir": "../out-test",
"sourceMap": true,
@@ -0,0 +1,87 @@
/**
* 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));
});
});
});
+3
View File
@@ -59,6 +59,7 @@ 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 &&
@@ -67,6 +68,8 @@ 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 &&
+7 -5
View File
@@ -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 { IOptionsService } from 'common/services/Services';
import type { ILogService, IOptionsService } from 'common/services/Services';
import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
interface IVertices {
@@ -114,7 +114,8 @@ export class GlyphRenderer extends Disposable {
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions,
private readonly _optionsService: IOptionsService
private readonly _optionsService: IOptionsService,
private readonly _logService: ILogService
) {
super();
@@ -127,7 +128,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._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages), this._logService));
this._register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
@@ -136,8 +137,9 @@ export class GlyphRenderer extends Disposable {
this._textureLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_texture'));
// Create and set the vertex array object
this._vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this._vertexArrayObject);
const vertexArrayObject = this._vertexArrayObject = gl.createVertexArray();
this._register(toDisposable(() => gl.deleteVertexArray(vertexArrayObject)));
gl.bindVertexArray(vertexArrayObject);
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
+7 -4
View File
@@ -14,6 +14,7 @@ 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,
@@ -89,21 +90,23 @@ export class RectangleRenderer extends Disposable {
private _terminal: Terminal,
private _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions,
private readonly _themeService: IThemeService
private readonly _themeService: IThemeService,
private readonly _logService: ILogService
) {
super();
const gl = this._gl;
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource, this._logService));
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
this._vertexArrayObject = gl.createVertexArray();
gl.bindVertexArray(this._vertexArrayObject);
const vertexArrayObject = this._vertexArrayObject = gl.createVertexArray();
this._register(toDisposable(() => gl.deleteVertexArray(vertexArrayObject)));
gl.bindVertexArray(vertexArrayObject);
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
+2 -1
View File
@@ -109,6 +109,7 @@ export class TextureAtlas implements ITextureAtlas {
page.canvas.remove();
}
this._onAddTextureAtlasCanvas.dispose();
this._onRemoveTextureAtlasCanvas.dispose();
}
public warmUp(): void {
@@ -529,7 +530,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, 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, this._logService, backgroundColor.css, variantOffset);
}
// Whether to clear pixels based on a threshold difference between the glyph color and the
+1
View File
@@ -28,6 +28,7 @@ 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;
}
+3 -1
View File
@@ -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, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { IWebGL2RenderingContext } from './Types';
import { WebglRenderer } from './WebglRenderer';
import { Emitter, EventUtils } from 'common/Event';
@@ -65,6 +65,7 @@ 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(
@@ -74,6 +75,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
coreBrowserService,
coreService,
decorationService,
logService,
optionsService,
themeService,
this._customGlyphs,
+11 -8
View File
@@ -15,7 +15,7 @@ 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, IOptionsService } from 'common/services/Services';
import { ICoreService, IDecorationService, ILogService, IOptionsService } from 'common/services/Services';
import { Terminal } from '@xterm/xterm';
import { GlyphRenderer } from './GlyphRenderer';
import { RectangleRenderer } from './RectangleRenderer';
@@ -77,6 +77,7 @@ 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,
@@ -122,19 +123,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) => {
console.log('webglcontextlost event received');
this._logService.debug('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;
console.warn('webgl context not restored; firing onContextLoss');
this._logService.warn('webgl context not restored; firing onContextLoss');
this._onContextLoss.fire(e);
}, 3000 /* ms */);
}));
this._register(addDisposableListener(this._canvas, 'webglcontextrestored', (e) => {
console.warn('webglcontextrestored event received');
this._logService.warn('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
// The texture atlas and glyph renderer must be fully reinitialized
@@ -158,6 +159,8 @@ 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();
}
@@ -274,8 +277,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._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService);
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);
// Update dimensions and acquire char atlas
this.handleCharSizeChanged();
@@ -424,8 +427,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let skipJoinedCheckUntilX: number = 0;
let isValidJoinRange: boolean = true;
let skipJoinedCheckUntilX: number;
let isValidJoinRange: boolean;
let lastCharX: number;
let range: [number, number];
let isCursorRow: boolean;
+7 -6
View File
@@ -4,6 +4,7 @@
*/
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,
@@ -16,21 +17,21 @@ export const PROJECTION_MATRIX = new Float32Array([
-1, 1, 0, 1
]);
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string): WebGLProgram | undefined {
export function createProgram(gl: WebGLRenderingContext, vertexSource: string, fragmentSource: string, logService: ILogService): WebGLProgram | undefined {
const program = throwIfFalsy(gl.createProgram());
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource)));
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)));
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.VERTEX_SHADER, vertexSource, logService)));
gl.attachShader(program, throwIfFalsy(createShader(gl, gl.FRAGMENT_SHADER, fragmentSource, logService)));
gl.linkProgram(program);
const success = gl.getProgramParameter(program, gl.LINK_STATUS);
if (success) {
return program;
}
console.error(gl.getProgramInfoLog(program));
logService.error(gl.getProgramInfoLog(program));
gl.deleteProgram(program);
}
export function createShader(gl: WebGLRenderingContext, type: number, source: string): WebGLShader | undefined {
export function createShader(gl: WebGLRenderingContext, type: number, source: string, logService: ILogService): WebGLShader | undefined {
const shader = throwIfFalsy(gl.createShader(type));
gl.shaderSource(shader, source);
gl.compileShader(shader);
@@ -39,7 +40,7 @@ export function createShader(gl: WebGLRenderingContext, type: number, source: st
return shader;
}
console.error(gl.getShaderInfoLog(shader));
logService.error(gl.getShaderInfoLog(shader));
gl.deleteShader(shader);
}
@@ -5,7 +5,7 @@
import { CustomGlyphDefinitionType, CustomGlyphScaleType, CustomGlyphVectorType, type CustomGlyphCharacterDefinition, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition } from './Types';
/* eslint-disable max-len */
/* oxlint-disable @stylistic/max-len -- generated glyph path data */
const enum Shapes {
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
@@ -4,6 +4,7 @@
*/
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';
@@ -22,6 +23,7 @@ export function tryDrawCustomGlyph(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
logService: ILogService,
backgroundColor?: string,
variantOffset: number = 0
): boolean {
@@ -30,7 +32,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, backgroundColor, variantOffset);
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, fontSize, devicePixelRatio, logService, backgroundColor, variantOffset);
}
return true;
}
@@ -49,6 +51,7 @@ function drawDefinitionPart(
deviceCharHeight: number,
fontSize: number,
devicePixelRatio: number,
logService: ILogService,
backgroundColor?: string,
variantOffset: number = 0
): void {
@@ -79,7 +82,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, part.strokeWidth);
drawPathFunctionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, logService, part.strokeWidth);
break;
case CustomGlyphDefinitionType.PATH:
drawPathDefinitionCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, devicePixelRatio, part.strokeWidth);
@@ -88,7 +91,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);
drawVectorShape(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight, fontSize, devicePixelRatio, logService);
break;
case CustomGlyphDefinitionType.BRAILLE:
drawBrailleCharacter(ctx, part.data, drawXOffset, drawYOffset, drawWidth, drawHeight);
@@ -510,6 +513,7 @@ function drawPathFunctionCharacter(
deviceCellWidth: number,
deviceCellHeight: number,
devicePixelRatio: number,
logService: ILogService,
strokeWidth?: number
): void {
ctx.save();
@@ -536,7 +540,7 @@ function drawPathFunctionCharacter(
}
const f = svgToCanvasInstructionMap[type];
if (!f) {
console.error(`Could not find drawing instructions for "${type}"`);
logService.error(`Could not find drawing instructions for "${type}"`);
continue;
}
const args: string[] = instruction.substring(1).split(',');
@@ -598,7 +602,8 @@ function drawVectorShape(
deviceCellWidth: number,
deviceCellHeight: number,
fontSize: number,
devicePixelRatio: number
devicePixelRatio: number,
logService: ILogService
): void {
// Clip the cell to make sure drawing doesn't occur beyond bounds
const clipRegion = new Path2D();
@@ -619,7 +624,7 @@ function drawVectorShape(
}
const f = svgToCanvasInstructionMap[type];
if (!f) {
console.error(`Could not find drawing instructions for "${type}"`);
logService.error(`Could not find drawing instructions for "${type}"`);
continue;
}
const args: string[] = instruction.substring(1).split(',');
-1
View File
@@ -25,7 +25,6 @@
]
},
"strict": true,
"downlevelIteration": true,
"experimentalDecorators": true,
"types": [
"../../../node_modules/@types/mocha"
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Copyright (c) 2026 The xterm.js authors. All rights reserved.
# @license MIT
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
if [[ ! -f src/common/parser/wasm/transition_table.h ]]; then
npm run build
node bin/gen_parser_transition_table.mjs
fi
WASM_DIR=src/common/parser/wasm
OUT="$WASM_DIR/parser.wasm"
# Apple Clang (Xcode CLT) has no wasm32 target; prefer Homebrew LLVM when needed.
wasm_clang_has_target() {
"$1" --print-targets 2>/dev/null | grep -q 'wasm32'
}
resolve_wasm_toolchain() {
local clang_bin=clang
if wasm_clang_has_target clang; then
:
else
local llvm_prefix=""
if command -v brew >/dev/null 2>&1; then
llvm_prefix="$(brew --prefix llvm 2>/dev/null || true)"
fi
for prefix in "$llvm_prefix" /opt/homebrew/opt/llvm /usr/local/opt/llvm; do
if [[ -n "$prefix" && -x "$prefix/bin/clang" ]] && wasm_clang_has_target "$prefix/bin/clang"; then
clang_bin="$prefix/bin/clang"
local lld_prefix=""
if command -v brew >/dev/null 2>&1; then
lld_prefix="$(brew --prefix lld 2>/dev/null || true)"
fi
for lld_dir in "$lld_prefix/bin" /opt/homebrew/opt/lld/bin /usr/local/opt/lld/bin; do
if [[ -n "$lld_dir" && -x "$lld_dir/lld" ]]; then
PATH="$lld_dir:$(dirname "$clang_bin"):$PATH"
break
fi
done
PATH="$(dirname "$clang_bin"):$PATH"
break
fi
done
if ! wasm_clang_has_target "$clang_bin"; then
echo "error: no clang with wasm32 support found (macOS: brew install llvm lld)" >&2
exit 1
fi
fi
WASM_CLANG="$clang_bin"
}
resolve_wasm_toolchain
"$WASM_CLANG" --target=wasm32-unknown-unknown -O2 -nostdlib -fuse-ld=lld \
-Wl,--no-entry \
-Wl,--export-memory \
-Wl,--export=reset \
-Wl,--export=scan \
-Wl,--export=probe_action \
-Wl,--export=set_state \
-Wl,--export=sync_params_from \
-Wl,--export=export_params_to \
-Wl,--export=get_params_len \
-Wl,--export=get_subparams_len \
-Wl,--export=get_subparams_ptr \
-Wl,--export=get_subparams_idx_ptr \
-Wl,--export=get_input_ptr \
-Wl,--export=get_kinds_ptr \
-Wl,--export=get_starts_ptr \
-Wl,--export=get_lengths_ptr \
-Wl,--export=get_aux_ptr \
-Wl,--export=get_param_starts_ptr \
-Wl,--export=get_param_counts_ptr \
-Wl,--export=get_params_ptr \
-Wl,--export=get_state_ptr \
-Wl,--export=get_header_ptr \
-Wl,--initial-memory=67108864 \
-Wl,--max-memory=67108864 \
"$WASM_DIR/parser.c" -o "$OUT"
node bin/embed_parser_wasm.mjs
echo "Built $OUT"
+23
View File
@@ -0,0 +1,23 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { readFileSync, writeFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
const wasm = readFileSync(join(root, 'src/common/parser/wasm/parser.wasm'));
const b64 = wasm.toString('base64');
const out = `/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
/** Base64-encoded parser.wasm (generated by bin/build_parser_wasm.sh) */
export const PARSER_WASM_BASE64 = '${b64}';\n`;
writeFileSync(join(root, 'src/common/parser/wasm/parser.wasm.embed.ts'), out);
console.info(`Embedded parser.wasm (${wasm.length} bytes)`);
+34
View File
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
// @ts-check
import { writeFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const root = join(__dirname, '..');
// Import compiled transition table
const { VT500_TRANSITION_TABLE } = await import(join(root, 'out/common/parser/EscapeSequenceParser.js'));
const table = VT500_TRANSITION_TABLE.table;
let out = '/* Auto-generated by bin/gen_parser_transition_table.mjs - do not edit */\n';
out += `#ifndef PARSER_TRANSITION_TABLE_H\n#define PARSER_TRANSITION_TABLE_H\n\n`;
out += `#include <stdint.h>\n\n`;
out += `#define PARSER_TABLE_SIZE ${table.length}\n\n`;
out += `static const uint16_t PARSER_TRANSITION_TABLE[PARSER_TABLE_SIZE] = {\n`;
for (let i = 0; i < table.length; i++) {
if (i % 16 === 0) out += ' ';
out += `0x${table[i].toString(16)}`;
out += i < table.length - 1 ? ', ' : '';
if (i % 16 === 15) out += '\n';
}
if (table.length % 16 !== 0) out += '\n';
out += `};\n\n#endif\n`;
const headerPath = join(root, 'src/common/parser/wasm/transition_table.h');
writeFileSync(headerPath, out);
console.info(`Wrote ${headerPath} (${table.length} entries)`);
+41 -11
View File
@@ -33,17 +33,47 @@ if (files.length === 0) {
console.log(`Linting ${files.length} changed file(s)...`);
const eslintArgs = ['--max-warnings', '0', '--no-warn-ignored'];
if (fix) {
eslintArgs.push('--fix');
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}`));
}
});
}));
}
eslintArgs.push(...files);
const eslint = process.platform === 'win32' ? 'eslint.cmd' : 'eslint';
const child = spawn(eslint, eslintArgs, {
stdio: 'inherit',
cwd: path.join(__dirname, '..'),
shell: process.platform === 'win32'
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);
});
child.on('close', code => process.exit(code ?? 0));
+1 -1
View File
@@ -8,7 +8,7 @@ const path = require('path');
const COVERAGE_LINES_THRESHOLD = 40;
// Add `out` to the NODE_PATH so absolute paths can be resolved.
// Add `out-esbuild` to NODE_PATH so addon path-mapped imports (common/*, browser/*) resolve.
const env = { ...process.env };
env.NODE_PATH = path.resolve(__dirname, '../out-esbuild');
@@ -65,7 +65,8 @@ 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');
this._addDdWithButton(dlIip, 'image-demo3', 'palette (png File)');
this._addDdWithButton(dlIip, 'image-demo4', 'dice (qoi MultipartFile)');
container.appendChild(dlIip);
// Kitty demos
@@ -141,6 +142,21 @@ 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 => {
@@ -158,6 +174,8 @@ 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'));
@@ -176,7 +194,7 @@ export class AddonImageWindow extends BaseWindow implements IControlWindow {
// }
// }
const pos = (this._terminal as any)._core._mouseService!.getCoords(ev, (this._terminal as any)._core.screenElement!, this._terminal.cols, this._terminal.rows);
const pos = (this._terminal as any)._core._mouseCoordsService!.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
+1 -1
View File
@@ -43,7 +43,7 @@ export class GpuWindow extends BaseWindow implements IControlWindow {
}
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
// eslint-disable-next-line no-restricted-syntax
// oxlint-disable-next-line eslint-js/no-restricted-syntax
const dpr = window.devicePixelRatio;
canvas.style.width = `${canvas.width / dpr}px`;
canvas.style.height = `${canvas.height / dpr}px`;
+1 -1
View File
@@ -43,7 +43,7 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
}
private _styleAtlasPage(canvas: HTMLCanvasElement): void {
// eslint-disable-next-line no-restricted-syntax
// oxlint-disable-next-line eslint-js/no-restricted-syntax
const dpr = window.devicePixelRatio;
canvas.style.width = `${canvas.width / dpr}px`;
canvas.style.height = `${canvas.height / dpr}px`;
+42 -13
View File
@@ -2,25 +2,54 @@
"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"
],
"*": ["./*"]
}
},
+1 -1
View File
@@ -118,7 +118,7 @@ function startServer(): void {
});
appWs.ws('/terminals/:pid', function (ws, req) {
const term = terminals[parseInt(req.params.pid)];
const term = terminals[parseInt(Array.isArray(req.params.pid) ? req.params.pid[0] : req.params.pid)];
console.log('Connected to terminal ' + term.pid);
temporaryDisposable[term.pid].dispose();
delete temporaryDisposable[term.pid];
+2 -1
View File
@@ -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: true }],
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/member-delimiter-style': ['warn', {
multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'comma', requireLast: false }
@@ -86,6 +86,7 @@ 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', {
+71
View File
@@ -0,0 +1,71 @@
// @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
}
}
);
+1 -1
View File
@@ -17,7 +17,7 @@ export default tseslint.config(
rules: {
'@stylistic/indent': ['warn', 2],
'@stylistic/semi': ['warn', 'always'],
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: true }],
'@stylistic/quotes': ['warn', 'single', { allowTemplateLiterals: 'always' }],
'@stylistic/member-delimiter-style': ['warn', {
multiline: { delimiter: 'semi', requireLast: true },
singleline: { delimiter: 'comma', requireLast: false }
+3133 -2292
View File
File diff suppressed because it is too large Load Diff
+36 -30
View File
@@ -34,6 +34,7 @@
"start": "node demo/start",
"dev": "concurrently -k -p [{name}] -n tsc,esbuild,esbuild-demo-client,esbuild-demo-server,server -c blue,yellow,cyan,green,magenta \"npm:tsc-watch\" \"npm:esbuild-watch\" \"npm:esbuild-demo-client-watch\" \"npm:esbuild-demo-server-watch\" \"npm:start\"",
"build": "npm run tsc",
"build:parser-wasm": "bash bin/build_parser_wasm.sh",
"watch": "npm run tsc-watch",
"tsc": "tsgo -b ./tsconfig.all.json",
"tsc-watch": "tsgo -b -w ./tsconfig.all.json --preserveWatchOutput",
@@ -48,11 +49,11 @@
"esbuild-demo-server-watch": "node bin/esbuild.mjs --demo-server --watch",
"test": "npm run test-unit",
"posttest": "npm run lint",
"lint": "eslint --max-warnings 0 src/ addons/ demo/ test/",
"lint": "oxlint --type-aware --max-warnings 0 src addons demo test && eslint --config eslint.config.naming.mjs --max-warnings 0 src addons demo test",
"lint-changes": "node ./bin/lint_changes.js",
"lint-changes-fix": "node ./bin/lint_changes.js --fix",
"lint-fix": "eslint --fix src/ addons/ demo/ test/",
"lint-api": "eslint --config eslint.config.typings.mjs --max-warnings 0 typings/",
"lint-fix": "oxlint --type-aware --fix --max-warnings 0 src addons demo test",
"lint-api": "oxlint --max-warnings 0 -c .oxlintrc.typings.json typings && eslint --config eslint.config.naming.mjs --max-warnings 0 typings",
"test-unit": "node ./bin/test_unit.js",
"test-unit-slow-tests": "npm run test-unit | grep \"ms)\"",
"test-unit-coverage": "node ./bin/test_unit.js --coverage",
@@ -61,9 +62,9 @@
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
"benchmark": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
"benchmark-baseline": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
"benchmark-eval": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
"clean": "rm -rf lib out addons/*/lib addons/*/out",
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
"prepackage": "npm run build",
@@ -75,40 +76,45 @@
"prepublishOnly": "npm run package"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@lunapaint/png-codec": "^0.2.0",
"@playwright/test": "^1.57.0",
"@stylistic/eslint-plugin": "^4.4.1",
"@types/chai": "^4.2.22",
"@types/express": "4",
"@playwright/test": "^1.59.1",
"@stylistic/eslint-plugin": "^5.10.0",
"@types/chai": "^5.2.3",
"@types/express": "^5.0.6",
"@types/express-ws": "^3.0.1",
"@types/jsdom": "^27.0.0",
"@types/mocha": "^9.0.0",
"@types/jsdom": "^28.0.1",
"@types/mocha": "^10.0.10",
"@types/node": "^22.19.3",
"@types/webpack": "^5.28.0",
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^8.50.1",
"@typescript-eslint/parser": "^8.50.1",
"@typescript/native-preview": "^7.0.0-dev.20260213.1",
"chai": "^4.3.4",
"@typescript-eslint/eslint-plugin": "^8.59.2",
"@typescript-eslint/parser": "^8.59.2",
"@typescript/native-preview": "^7.0.0-dev.20260509.2",
"chai": "^6.2.2",
"concurrently": "^9.1.2",
"esbuild": "~0.25.2",
"eslint": "^9.39.2",
"eslint-plugin-jsdoc": "^50.8.0",
"express": "^4.19.2",
"esbuild": "^0.28.0",
"eslint": "^10.3.0",
"eslint-plugin-jsdoc": "^62.9.0",
"eslint-plugin-oxlint": "^1.67.0",
"express": "^5.2.1",
"express-ws": "^5.0.2",
"jsdom": "^27.3.0",
"mocha": "^10.1.0",
"jsdom": "^29.1.1",
"mocha": "^11.7.5",
"mustache": "^4.2.0",
"node-pty": "^1.2.0-beta.9",
"nyc": "^17.1.0",
"source-map-loader": "^3.0.0",
"nyc": "^18.0.0",
"oxlint": "^1.67.0",
"oxlint-plugin-eslint": "^1.67.0",
"oxlint-tsgolint": "^0.23.0",
"source-map-loader": "^5.0.0",
"source-map-support": "^0.5.20",
"ts-loader": "^9.3.1",
"ts-loader": "^9.5.7",
"typescript": "^5.9.3",
"typescript-eslint": "^8.50.1",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
"ws": "^8.2.3",
"xterm-benchmark": "^0.3.1"
"typescript-eslint": "^8.59.2",
"webpack": "^5.106.2",
"webpack-cli": "^7.0.2",
"ws": "^8.20.0",
"xterm-benchmark": "^0.3.2"
}
}
+8 -8
View File
@@ -3,14 +3,14 @@
* @license MIT
*/
import * as Strings from 'browser/LocalizableStrings';
import { ITerminal, IRenderDebouncer } from 'browser/Types';
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { IBuffer } from 'common/buffer/Types';
import { IInstantiationService } from 'common/services/Services';
import { addDisposableListener } from 'browser/Dom';
import * as Strings from './LocalizableStrings';
import { ITerminal, IRenderDebouncer } from './Types';
import { TimeBasedDebouncer } from './TimeBasedDebouncer';
import { Disposable, toDisposable } from '../common/Lifecycle';
import { ICoreBrowserService, IRenderService } from './services/Services';
import { IBuffer } from '../common/buffer/Types';
import { IInstantiationService } from '../common/services/Services';
import { addDisposableListener } from './Dom';
const enum Constants {
MAX_ROWS_TO_READ = 20
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import * as Clipboard from 'browser/Clipboard';
import * as Clipboard from './Clipboard';
describe('evaluatePastedTextProcessing', () => {
it('should replace carriage return and/or line feed with carriage return', () => {
+2 -2
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ISelectionService } from 'browser/services/Services';
import { ICoreService, IOptionsService } from 'common/services/Services';
import { ISelectionService } from './services/Services';
import { ICoreService, IOptionsService } from '../common/services/Services';
/**
* Prepares text to be pasted into the terminal by normalizing the line endings
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorContrastCache } from './ColorContrastCache';
describe('ColorContrastCache', () => {
let cache: ColorContrastCache;
+3 -3
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IColorContrastCache } from 'browser/Types';
import { IColor } from 'common/Types';
import { TwoKeyMap } from 'common/MultiKeyMap';
import { IColorContrastCache } from './Types';
import { IColor } from '../common/Types';
import { TwoKeyMap } from '../common/MultiKeyMap';
export class ColorContrastCache implements IColorContrastCache {
private _color: TwoKeyMap</* bg */number, /* fg */number, IColor | null> = new TwoKeyMap();
+40 -38
View File
@@ -22,43 +22,43 @@
*/
import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker, IRenderDimensions as IRenderDimensionsApi } from '@xterm/xterm';
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard';
import * as Strings from 'browser/LocalizableStrings';
import { OscLinkProvider } from 'browser/OscLinkProvider';
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from 'browser/Types';
import { Viewport } from 'browser/Viewport';
import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IRenderer } from 'browser/renderer/shared/Types';
import { CharSizeService } from 'browser/services/CharSizeService';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MouseCoordsService } from 'browser/services/MouseCoordsService';
import { MouseService } from 'browser/services/MouseService';
import { RenderService } from 'browser/services/RenderService';
import { SelectionService } from 'browser/services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ThemeService } from 'browser/services/ThemeService';
import { KeyboardService } from 'browser/services/KeyboardService';
import { channels, color, rgb } from 'common/Color';
import { CoreTerminal } from 'common/CoreTerminal';
import * as Browser from 'common/Platform';
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBuffer } from 'common/buffer/Types';
import { C0, C1ESCAPED } from 'common/data/EscapeSequences';
import { toRgbString } from 'common/input/XParseColor';
import { DecorationService } from 'common/services/DecorationService';
import { IDecorationService } from 'common/services/Services';
import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from './Clipboard';
import * as Strings from './LocalizableStrings';
import { OscLinkProvider } from './OscLinkProvider';
import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal } from './Types';
import { Viewport } from './Viewport';
import { BufferDecorationRenderer } from './decorations/BufferDecorationRenderer';
import { OverviewRulerRenderer } from './decorations/OverviewRulerRenderer';
import { CompositionHelper } from './input/CompositionHelper';
import { DomRenderer } from './renderer/dom/DomRenderer';
import { IRenderer } from './renderer/shared/Types';
import { CharSizeService } from './services/CharSizeService';
import { CharacterJoinerService } from './services/CharacterJoinerService';
import { CoreBrowserService } from './services/CoreBrowserService';
import { LinkProviderService } from './services/LinkProviderService';
import { MouseCoordsService } from './services/MouseCoordsService';
import { MouseEventCssClasses, MouseService } from './services/MouseService';
import { RenderService } from './services/RenderService';
import { SelectionService } from './services/SelectionService';
import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IKeyboardService, ILinkProviderService, IMouseCoordsService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
import { ThemeService } from './services/ThemeService';
import { KeyboardService } from './services/KeyboardService';
import { channels, color, rgb } from '../common/Color';
import { CoreTerminal } from '../common/CoreTerminal';
import * as Browser from '../common/Platform';
import { ColorRequestType, IColorEvent, ITerminalOptions, KeyboardResultType, SpecialColorIndex } from '../common/Types';
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';
import { IBuffer } from '../common/buffer/Types';
import { C0, C1ESCAPED } from '../common/data/EscapeSequences';
import { toRgbString } from '../common/input/XParseColor';
import { DecorationService } from '../common/services/DecorationService';
import { IDecorationService } from '../common/services/Services';
import { WindowsOptionsReportType } from '../common/InputHandler';
import { AccessibilityManager } from './AccessibilityManager';
import { Linkifier } from './Linkifier';
import { Emitter, EventUtils, type IEvent } from 'common/Event';
import { addDisposableListener } from 'browser/Dom';
import { MutableDisposable, toDisposable } from 'common/Lifecycle';
import { Emitter, EventUtils, type IEvent } from '../common/Event';
import { addDisposableListener } from './Dom';
import { MutableDisposable, toDisposable } from '../common/Lifecycle';
export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
public textarea: HTMLTextAreaElement | undefined;
@@ -212,7 +212,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
if (!this._themeService) return;
for (const req of event) {
let acc: 'foreground' | 'background' | 'cursor' | 'ansi';
let ident = '';
let ident: string;
switch (req.index) {
case SpecialColorIndex.FOREGROUND: // OSC 10 | 110
acc = 'foreground';
@@ -563,8 +563,9 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
try {
this._onWillOpen.fire(this.element);
} catch (e) {
this._logService.error('onWillOpen handler threw an exception', e);
}
catch { /* fails to load addon for some reason */ }
if (!this._renderService.hasRenderer()) {
this._renderService.setRenderer(this._createRenderer());
}
@@ -617,11 +618,12 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
this._register(addDisposableListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
// apply mouse event classes set by escape codes before terminal was attached
if (this.mouseStateService.areMouseEventsActive) {
if (this.mouseStateService.areMouseEventsActive && !this.options.mouseEventsRequireAlt) {
this._selectionService.disable();
this.element.classList.add('enable-mouse-events');
this.element.classList.add(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
} else {
this._selectionService.enable();
this.element.classList.remove(MouseEventCssClasses.ENABLE_MOUSE_EVENTS);
}
if (this.options.screenReaderMode) {
+2 -2
View File
@@ -5,8 +5,8 @@
* Minimal DOM helpers for xterm.js browser code.
*/
import { IntervalTimer } from 'common/Async';
import { IDisposable } from 'common/Lifecycle';
import { IntervalTimer } from '../common/Async';
import { IDisposable } from '../common/Lifecycle';
export function getWindow(e: Node | UIEvent | undefined | null): Window {
const candidateNode = e as Node | undefined | null;
+44 -4
View File
@@ -4,11 +4,11 @@
*/
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
import { IBufferService } from '../common/services/Services';
import { Linkifier } from './Linkifier';
import { MockBufferService } from 'common/TestUtils.test';
import { ILink } from 'browser/Types';
import { LinkProviderService } from 'browser/services/LinkProviderService';
import { MockBufferService } from '../common/TestUtils.test';
import { ILink } from './Types';
import { LinkProviderService } from './services/LinkProviderService';
import jsdom = require('jsdom');
class TestLinkifier2 extends Linkifier {
@@ -43,6 +43,20 @@ describe('Linkifier2', () => {
},
activate: () => { }
};
const multiLineLink: ILink = {
text: 'foo',
range: {
start: {
x: 2,
y: 1
},
end: {
x: 4,
y: 2
}
},
activate: () => { }
};
beforeEach(() => {
const dom = new jsdom.JSDOM();
@@ -86,4 +100,30 @@ describe('Linkifier2', () => {
linkifier.linkLeave({ classList: { add: () => { } } } as any, link, {} as any);
});
it('onShowLinkUnderline event range is correct for wrapped links', done => {
linkifier.onShowLinkUnderline(e => {
assert.equal(multiLineLink.range.start.x - 1, e.x1);
assert.equal(multiLineLink.range.start.y - 1, e.y1);
assert.equal(multiLineLink.range.end.x, e.x2);
assert.equal(multiLineLink.range.end.y - 1, e.y2);
done();
});
linkifier.linkHover({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
});
it('onHideLinkUnderline event range is correct for wrapped links', done => {
linkifier.onHideLinkUnderline(e => {
assert.equal(multiLineLink.range.start.x - 1, e.x1);
assert.equal(multiLineLink.range.start.y - 1, e.y1);
assert.equal(multiLineLink.range.end.x, e.x2);
assert.equal(multiLineLink.range.end.y - 1, e.y2);
done();
});
linkifier.linkLeave({ classList: { add: () => { } } } as any, multiLineLink, {} as any);
});
});
+6 -6
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { Disposable, dispose, toDisposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
import { IBufferService } from 'common/services/Services';
import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from './Types';
import { Disposable, dispose, toDisposable } from '../common/Lifecycle';
import { IDisposable } from '../common/Types';
import { IBufferService } from '../common/services/Services';
import { ILinkProviderService, IMouseCoordsService, IRenderService } from './services/Services';
import { Emitter } from 'common/Event';
import { addDisposableListener } from 'browser/Dom';
import { Emitter } from '../common/Event';
import { addDisposableListener } from './Dom';
export class Linkifier extends Disposable implements ILinkifier2 {
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { OscLinkProvider } from './OscLinkProvider';
import { ILink } from './Types';
import { createCellData, MockBufferService, MockOptionsService } from '../common/TestUtils.test';
import { IBufferService, IOscLinkService } from '../common/services/Services';
import { IBufferLine, IOscLinkData } from '../common/Types';
class TestOscLinkService implements IOscLinkService {
public serviceBrand: any;
public registerLink(_linkData: IOscLinkData): number { return 0; }
public addLineToLink(_linkId: number, _y: number): void { }
public getLinkData(linkId: number): IOscLinkData | undefined {
return { uri: `https://example.com/${linkId}` };
}
}
function setText(line: IBufferLine | undefined, x: number, text: string): void {
if (!line) {
throw new Error('Missing buffer line');
}
for (let i = 0; i < text.length; i++) {
line.setCell(x + i, createCellData(0, text[i], 1));
}
}
function setUrl(line: IBufferLine | undefined, x: number, text: string, linkId: number): void {
if (!line) {
throw new Error('Missing buffer line');
}
for (let i = 0; i < text.length; i++) {
const cell = createCellData(0, text[i], 1);
cell.extended.urlId = linkId;
cell.updateExtended();
line.setCell(x + i, cell);
}
}
function getLinks(provider: OscLinkProvider, y: number): Promise<ILink[]> {
return new Promise(resolve => provider.provideLinks(y, links => resolve(links ?? [])));
}
describe('OscLinkProvider', () => {
let bufferService: IBufferService;
let provider: OscLinkProvider;
beforeEach(() => {
const optionsService = new MockOptionsService();
bufferService = new MockBufferService(5, 5, optionsService);
provider = new OscLinkProvider(bufferService, optionsService, new TestOscLinkService());
});
it('expands a wrapped link range backward to the previous line', async () => {
const line1 = bufferService.buffer.lines.get(0);
const line2 = bufferService.buffer.lines.get(1);
setText(line1, 0, 'aa');
setUrl(line1, 2, 'bbb', 1);
setUrl(line2, 0, 'cccc', 1);
setText(line2, 4, 'x');
line2!.isWrapped = true;
const links = await getLinks(provider, 2);
assert.lengthOf(links, 1);
assert.deepEqual(links[0].range, {
start: { x: 3, y: 1 },
end: { x: 4, y: 2 }
});
});
it('expands a wrapped link range forward when a link ends at line boundary', async () => {
const line1 = bufferService.buffer.lines.get(0);
const line2 = bufferService.buffer.lines.get(1);
setUrl(line1, 0, 'aaaaa', 1);
setUrl(line2, 0, 'bb', 1);
setText(line2, 2, 'ccc');
line2!.isWrapped = true;
const links = await getLinks(provider, 1);
assert.lengthOf(links, 1);
assert.deepEqual(links[0].range, {
start: { x: 1, y: 1 },
end: { x: 2, y: 2 }
});
});
it('does not merge wrapped links with different url ids', async () => {
const line1 = bufferService.buffer.lines.get(0);
const line2 = bufferService.buffer.lines.get(1);
setUrl(line1, 0, 'aaaaa', 1);
setUrl(line2, 0, 'bbb', 2);
setText(line2, 3, 'cc');
line2!.isWrapped = true;
const links = await getLinks(provider, 1);
assert.lengthOf(links, 1);
assert.deepEqual(links[0].range, {
start: { x: 1, y: 1 },
end: { x: 5, y: 1 }
});
});
});
+83 -17
View File
@@ -3,10 +3,11 @@
* @license MIT
*/
import { IBufferRange, ILink } from 'browser/Types';
import { ILinkProvider } from 'browser/services/Services';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services';
import { IBufferRange, ILink } from './Types';
import { ILinkProvider } from './services/Services';
import { CellData } from '../common/buffer/CellData';
import { IBufferLine } from '../common/Types';
import { IBufferService, IOptionsService, IOscLinkService } from '../common/services/Services';
export class OscLinkProvider implements ILinkProvider {
private readonly _workCell = new CellData();
@@ -57,19 +58,8 @@ export class OscLinkProvider implements ILinkProvider {
if (finishLink || (currentStart !== -1 && x === lineLength - 1)) {
const text = this._oscLinkService.getLinkData(currentLinkId)?.uri;
if (text) {
// These ranges are 1-based
const range: IBufferRange = {
start: {
x: currentStart + 1,
y
},
end: {
// Offset end x if it's a link that ends on the last cell in the line
x: x + (!finishLink && x === lineLength - 1 ? 1 : 0),
y
}
};
const endX = x + (!finishLink && x === lineLength - 1 ? 1 : 0);
const range = this._getRangeWithLineWrap(y, currentStart, endX, currentLinkId);
let ignoreLink = false;
if (!linkHandler?.allowNonHttpProtocols) {
try {
@@ -111,6 +101,82 @@ export class OscLinkProvider implements ILinkProvider {
// id
callback(result);
}
/**
* Expand a single-line OSC 8 range to a contiguous wrapped range for the same link id.
*/
private _getRangeWithLineWrap(y: number, startX: number, endX: number, linkId: number): IBufferRange {
let startY = y;
let finalStartX = startX;
let endY = y;
let finalEndX = endX;
// Expand upward only when this segment starts at column 0 and the current line is wrapped.
while (finalStartX === 0) {
const currentLine = this._bufferService.buffer.lines.get(startY - 1);
if (!currentLine?.isWrapped) {
break;
}
const previousLine = this._bufferService.buffer.lines.get(startY - 2);
if (!previousLine) {
break;
}
const previousLineLength = previousLine.getTrimmedLength();
if (previousLineLength === 0 || !this._hasUrlId(previousLine, previousLineLength - 1, linkId)) {
break;
}
let previousStartX = previousLineLength - 1;
while (previousStartX > 0 && this._hasUrlId(previousLine, previousStartX - 1, linkId)) {
previousStartX--;
}
startY--;
finalStartX = previousStartX;
}
// Expand downward only when this segment reaches trimmed EOL and the next line is wrapped.
while (true) {
const currentLine = this._bufferService.buffer.lines.get(endY - 1);
if (!currentLine) {
break;
}
const currentLineLength = currentLine.getTrimmedLength();
if (finalEndX !== currentLineLength) {
break;
}
const nextLine = this._bufferService.buffer.lines.get(endY);
if (!nextLine?.isWrapped) {
break;
}
const nextLineLength = nextLine.getTrimmedLength();
if (nextLineLength === 0 || !this._hasUrlId(nextLine, 0, linkId)) {
break;
}
let nextEndX = 1;
while (nextEndX < nextLineLength && this._hasUrlId(nextLine, nextEndX, linkId)) {
nextEndX++;
}
endY++;
finalEndX = nextEndX;
}
// IBufferRange uses 1-based coordinates.
return {
start: {
x: finalStartX + 1,
y: startY
},
end: {
x: finalEndX,
y: endY
}
};
}
private _hasUrlId(line: IBufferLine, x: number, linkId: number): boolean {
const cell = this._workCell;
line.loadCell(x, cell);
return !!cell.hasExtendedAttrs() && cell.extended.urlId === linkId;
}
}
function defaultActivate(e: MouseEvent, uri: string): void {
+2 -2
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { IRenderDebouncerWithCallback } from 'browser/Types';
import { ICoreBrowserService } from 'browser/services/Services';
import { IRenderDebouncerWithCallback } from './Types';
import { ICoreBrowserService } from './services/Services';
/**
* Debounces calls to render terminal rows using animation frames.
+6 -6
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { MockCompositionHelper, MockRenderer, MockViewport, TestTerminal } from 'browser/TestUtils.test';
import type { IBrowser } from 'browser/Types';
import { MockCompositionHelper, MockRenderer, MockViewport, TestTerminal } from './TestUtils.test';
import type { IBrowser } from './Types';
import { assert } from 'chai';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { MockUnicodeService, createCellData } from 'common/TestUtils.test';
import { IMarker } from 'common/Types';
import { DEFAULT_ATTR_DATA } from '../common/buffer/BufferLine';
import { CellData } from '../common/buffer/CellData';
import { MockUnicodeService, createCellData } from '../common/TestUtils.test';
import { IMarker } from '../common/Types';
const INIT_COLS = 80;
const INIT_ROWS = 24;
+3 -3
View File
@@ -7,14 +7,14 @@ import * as path from 'path';
import * as os from 'os';
import * as fs from 'fs';
import * as pty from 'node-pty';
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
import { CoreBrowserTerminal } from './CoreBrowserTerminal';
import { IDisposable } from '@xterm/xterm';
// all test files expect terminal in 80x25
const COLS = 80;
const ROWS = 25;
const escapeSequenceFilesDir = path.join(__dirname, '../../fixtures/escape_sequence_files');
const escapeSequenceFilesDir = path.join(__dirname, '../../test/fixtures/escape_sequence_files');
const TESTFILES = fs.readdirSync(escapeSequenceFilesDir)
.filter(f => f.endsWith('.in'))
.map(f => path.join(escapeSequenceFilesDir, f));
@@ -125,7 +125,7 @@ function formatError(input: string, output: string, expected: string): string {
// simple debug output of terminal cells
function terminalToString(term: CoreBrowserTerminal): string {
let result = '';
let lineText = '';
let lineText: string;
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
lineText = term.buffer.lines.get(line)!.translateToString(true);
// rtrim empty cells as xterm does
+15 -15
View File
@@ -4,21 +4,21 @@
*/
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration, IRenderDimensions as IRenderDimensionsApi } from '@xterm/xterm';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from 'browser/Types';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from 'common/services/Services';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { css } from 'common/Color';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { Emitter, type IEvent } from 'common/Event';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from './services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from './renderer/shared/Types';
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet, IBufferElementProvider } from './Types';
import { IBuffer, IBufferSet } from '../common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from '../common/Types';
import { Buffer } from '../common/buffer/Buffer';
import * as Browser from '../common/Platform';
import { CoreBrowserTerminal } from './CoreBrowserTerminal';
import { IUnicodeService, IOptionsService, ICoreService, IMouseStateService } from '../common/services/Services';
import { IFunctionIdentifier, IParams } from '../common/parser/Types';
import { AttributeData } from '../common/buffer/AttributeData';
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from './selection/Types';
import { css } from '../common/Color';
import { createRenderDimensions } from './renderer/shared/RendererUtils';
import { Emitter, type IEvent } from '../common/Event';
export class TestTerminal extends CoreBrowserTerminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
+1 -1
View File
@@ -5,7 +5,7 @@
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
import { IRenderDebouncer } from 'browser/Types';
import { IRenderDebouncer } from './Types';
/**
* Debounces calls to update screen readers to update at most once configurable interval of time.
+4 -4
View File
@@ -3,11 +3,11 @@
* @license MIT
*/
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
import { CharData, IColor, ICoreTerminal, ITerminalOptions } from '../common/Types';
import { IBuffer } from '../common/buffer/Types';
import { IDisposable, IRenderDimensions as IRenderDimensionsApi, Terminal as ITerminalApi } from '@xterm/xterm';
import { channels, css } from 'common/Color';
import type { IEvent } from 'common/Event';
import { channels, css } from '../common/Color';
import type { IEvent } from '../common/Event';
/**
* A portion of the public API that are implemented identially internally and simply passed through.
+10 -10
View File
@@ -3,16 +3,16 @@
* @license MIT
*/
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ViewportConstants } from 'browser/shared/Constants';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from 'common/services/Services';
import { CoreMouseEventType } from 'common/Types';
import { scheduleAtNextAnimationFrame } from 'browser/Dom';
import { SmoothScrollableElement } from 'browser/scrollable/scrollableElement';
import type { IScrollableElementChangeOptions } from 'browser/scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from 'common/Event';
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from 'browser/scrollable/scrollable';
import { ICoreBrowserService, IRenderService, IThemeService } from './services/Services';
import { ViewportConstants } from './shared/Constants';
import { Disposable, toDisposable } from '../common/Lifecycle';
import { IBufferService, ICoreService, IMouseStateService, IOptionsService } from '../common/services/Services';
import { CoreMouseEventType } from '../common/Types';
import { scheduleAtNextAnimationFrame } from './Dom';
import { SmoothScrollableElement } from './scrollable/scrollableElement';
import type { IScrollableElementChangeOptions } from './scrollable/scrollableElementOptions';
import { Emitter, EventUtils } from '../common/Event';
import { Scrollable, ScrollbarVisibility, type IScrollEvent } from './scrollable/scrollable';
export class Viewport extends Disposable {
@@ -3,9 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
import { ICoreBrowserService, IRenderService } from '../services/Services';
import { Disposable, toDisposable } from '../../common/Lifecycle';
import { IBufferService, IDecorationService, IInternalDecoration } from '../../common/services/Services';
export class BufferDecorationRenderer extends Disposable {
private readonly _container: HTMLElement;
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ColorZoneStore } from 'browser/decorations/ColorZoneStore';
import { ColorZoneStore } from './ColorZoneStore';
const optionsRedFull = {
overviewRulerOptions: {
+1 -1
View File
@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IInternalDecoration } from 'common/services/Services';
import { IInternalDecoration } from '../../common/services/Services';
export interface IColorZoneStore {
readonly zones: IColorZone[];
@@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
import { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ColorZoneStore, IColorZone, IColorZoneStore } from './ColorZoneStore';
import { ICoreBrowserService, IRenderService, IThemeService } from '../services/Services';
import { Disposable, toDisposable } from '../../common/Lifecycle';
import { IBufferService, IDecorationService, IOptionsService } from '../../common/services/Services';
const enum Constants {
OVERVIEW_RULER_BORDER_WIDTH = 1
+3 -3
View File
@@ -4,9 +4,9 @@
*/
import { assert } from 'chai';
import { CompositionHelper } from 'browser/input/CompositionHelper';
import { MockRenderService } from 'browser/TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
import { CompositionHelper } from './CompositionHelper';
import { MockRenderService } from '../TestUtils.test';
import { MockCoreService, MockBufferService, MockOptionsService } from '../../common/TestUtils.test';
describe('CompositionHelper', () => {
let compositionHelper: CompositionHelper;
+3 -3
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IRenderService } from 'browser/services/Services';
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
import { C0 } from 'common/data/EscapeSequences';
import { IRenderService } from '../services/Services';
import { IBufferService, ICoreService, IOptionsService } from '../../common/services/Services';
import { C0 } from '../../common/data/EscapeSequences';
interface IPosition {
start: number;
+1 -1
View File
@@ -5,7 +5,7 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { getCoords } from 'browser/input/Mouse';
import { getCoords } from './Mouse';
const CHAR_WIDTH = 10;
const CHAR_HEIGHT = 20;
+2 -2
View File
@@ -4,8 +4,8 @@
*/
import { assert } from 'chai';
import { IBufferService } from 'common/services/Services';
import { MockBufferService } from 'common/TestUtils.test';
import { IBufferService } from '../../common/services/Services';
import { MockBufferService } from '../../common/TestUtils.test';
import { moveToCellSequence } from './MoveToCell';
describe('MoveToCell', () => {
+2 -2
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { C0 } from 'common/data/EscapeSequences';
import { IBufferService } from 'common/services/Services';
import { C0 } from '../../common/data/EscapeSequences';
import { IBufferService } from '../../common/services/Services';
const enum Direction {
UP = 'A',
+10 -10
View File
@@ -3,17 +3,17 @@
* @license MIT
*/
import * as Strings from 'browser/LocalizableStrings';
import { CoreBrowserTerminal as TerminalCore } from 'browser/CoreBrowserTerminal';
import { IBufferRange, ITerminal } from 'browser/Types';
import { Disposable } from 'common/Lifecycle';
import { ITerminalOptions } from 'common/Types';
import { AddonManager } from 'common/public/AddonManager';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import * as Strings from '../LocalizableStrings';
import { CoreBrowserTerminal as TerminalCore } from '../CoreBrowserTerminal';
import { IBufferRange, ITerminal } from '../Types';
import { Disposable } from '../../common/Lifecycle';
import { ITerminalOptions } from '../../common/Types';
import { AddonManager } from '../../common/public/AddonManager';
import { BufferNamespaceApi } from '../../common/public/BufferNamespaceApi';
import { ParserApi } from '../../common/public/ParserApi';
import { UnicodeApi } from '../../common/public/UnicodeApi';
import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOptions, IDisposable, ILinkProvider, ILocalizableStrings, IMarker, IModes, IParser, IRenderDimensions, ITerminalAddon, Terminal as ITerminalApi, ITerminalInitOnlyOptions, IUnicodeHandling } from '@xterm/xterm';
import type { IEvent } from 'common/Event';
import type { IEvent } from '../../common/Event';
/**
* The set of options that only have an effect when set in the Terminal constructor.
+14 -14
View File
@@ -3,20 +3,20 @@
* @license MIT
*/
import { DomRendererRowFactory, RowCss } from 'browser/renderer/dom/DomRendererRowFactory';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { INVERTED_DEFAULT_COLOR, RendererConstants } from 'browser/renderer/shared/Constants';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from 'browser/Types';
import { color } from 'common/Color';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from 'common/services/Services';
import { Emitter } from 'common/Event';
import { addDisposableListener } from 'browser/Dom';
import { DomRendererRowFactory, RowCss } from './DomRendererRowFactory';
import { WidthCache } from './WidthCache';
import { INVERTED_DEFAULT_COLOR, RendererConstants } from '../shared/Constants';
import { createRenderDimensions } from '../shared/RendererUtils';
import { createSelectionRenderModel } from '../shared/SelectionRenderModel';
import { TextBlinkStateManager } from '../shared/TextBlinkStateManager';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ISelectionRenderModel } from '../shared/Types';
import { ICharSizeService, ICoreBrowserService, IThemeService } from '../../services/Services';
import { ILinkifier2, ILinkifierEvent, ITerminal, ReadonlyColorSet } from '../../Types';
import { color } from '../../../common/Color';
import { Disposable, toDisposable } from '../../../common/Lifecycle';
import { IBufferService, ICoreService, IInstantiationService, IOptionsService } from '../../../common/services/Services';
import { Emitter } from '../../../common/Event';
import { addDisposableListener } from '../../Dom';
const enum Constants {
@@ -5,15 +5,15 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from 'common/buffer/Constants';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from 'common/TestUtils.test';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
import { TestWidthCache } from 'browser/renderer/dom/WidthCache.test';
import { DomRendererRowFactory } from './DomRendererRowFactory';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags, BgFlags, Attributes, UnderlineStyle } from '../../../common/buffer/Constants';
import { BufferLine, DEFAULT_ATTR_DATA } from '../../../common/buffer/BufferLine';
import { BufferLineStringCache } from '../../../common/buffer/BufferLineStringCache';
import { IBufferLine } from '../../../common/Types';
import { CellData } from '../../../common/buffer/CellData';
import { MockCoreService, MockDecorationService, MockOptionsService, createCellData, NULL_CELL_DATA } from '../../../common/TestUtils.test';
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from '../../TestUtils.test';
import { TestWidthCache } from './WidthCache.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
@@ -3,18 +3,18 @@
* @license MIT
*/
import { IBufferLine, ICellData, IColor } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { channels, color } from 'common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils';
import { AttributeData } from 'common/buffer/AttributeData';
import { WidthCache } from 'browser/renderer/dom/WidthCache';
import { IColorContrastCache } from 'browser/Types';
import { IBufferLine, ICellData, IColor } from '../../../common/Types';
import { INVERTED_DEFAULT_COLOR } from '../shared/Constants';
import { WHITESPACE_CELL_CHAR, Attributes } from '../../../common/buffer/Constants';
import { CellData } from '../../../common/buffer/CellData';
import { ICoreService, IDecorationService, IOptionsService } from '../../../common/services/Services';
import { channels, color } from '../../../common/Color';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from '../../services/Services';
import { JoinedCellData } from '../../services/CharacterJoinerService';
import { treatGlyphAsBackgroundColor } from '../shared/RendererUtils';
import { AttributeData } from '../../../common/buffer/AttributeData';
import { WidthCache } from './WidthCache';
import { IColorContrastCache } from '../../Types';
export const enum RowCss {
@@ -90,14 +90,14 @@ export class DomRendererRowFactory {
let charElement: HTMLSpanElement | undefined;
let cellAmount = 0;
let text = '';
let i = 0;
let i;
let oldBg = 0;
let oldFg = 0;
let oldExt = 0;
let oldLinkHover: number | boolean = false;
let oldSpacing = 0;
let oldIsInSelection: boolean = false;
let spacing = 0;
let spacing;
let skipJoinedCheckUntilX = 0;
const classes: string[] = [];
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import * as assert from 'assert';
import { IWidthCacheFontVariantCanvas, WidthCache, WidthCacheSettings } from 'browser/renderer/dom/WidthCache';
import { IWidthCacheFontVariantCanvas, WidthCache, WidthCacheSettings } from './WidthCache';
class MockWidthCacheFontVariantCanvas implements IWidthCacheFontVariantCanvas {
+4 -4
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IDisposable } from 'common/Types';
import { FontWeight } from 'common/services/Services';
import { throwIfFalsy } from '../shared/RendererUtils';
import { IDisposable } from '../../../common/Types';
import { FontWeight } from '../../../common/services/Services';
export const enum WidthCacheSettings {
@@ -110,7 +110,7 @@ export class WidthCache implements IDisposable {
* `variant` denotes the font variant to be used.
*/
public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0;
let cp: number;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
return this._flat[cp];
@@ -3,7 +3,7 @@
* @license MIT
*/
import { computeNextVariantOffset } from 'browser/renderer/shared/RendererUtils';
import { computeNextVariantOffset } from './RendererUtils';
import { assert } from 'chai';
describe('RendererUtils', () => {
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IDimensions, IRenderDimensions } from 'browser/renderer/shared/Types';
import { IDimensions, IRenderDimensions } from './Types';
export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ITerminal } from 'browser/Types';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { ITerminal } from '../../Types';
import { ISelectionRenderModel } from './Types';
import { Terminal } from '@xterm/xterm';
class SelectionRenderModel implements ISelectionRenderModel {
@@ -4,10 +4,10 @@
*/
import { assert } from 'chai';
import { TextBlinkStateManager } from 'browser/renderer/shared/TextBlinkStateManager';
import { MockOptionsService } from 'common/TestUtils.test';
import type { ICoreBrowserService } from 'browser/services/Services';
import { Emitter } from 'common/Event';
import { TextBlinkStateManager } from './TextBlinkStateManager';
import { MockOptionsService } from '../../../common/TestUtils.test';
import type { ICoreBrowserService } from '../../services/Services';
import { Emitter } from '../../../common/Event';
class FakeWindow {
public nextId = 1;
@@ -3,9 +3,9 @@
* @license MIT
*/
import { ICoreBrowserService } from 'browser/services/Services';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IOptionsService } from 'common/services/Services';
import { ICoreBrowserService } from '../../services/Services';
import { Disposable, toDisposable } from '../../../common/Lifecycle';
import { IOptionsService } from '../../../common/services/Services';
export class TextBlinkStateManager extends Disposable {
private _intervalDuration: number = 0;
+3 -3
View File
@@ -4,9 +4,9 @@
*/
import { Terminal } from '@xterm/xterm';
import { ITerminal } from 'browser/Types';
import { IDisposable } from 'common/Types';
import type { IEvent } from 'common/Event';
import { ITerminal } from '../../Types';
import { IDisposable } from '../../../common/Types';
import type { IEvent } from '../../../common/Event';
export interface IDimensions {
width: number;

Some files were not shown because too many files have changed in this diff Show More