Compare commits

...
Author SHA1 Message Date
Jörg Breitbart f9c38786bb fix IIP resize, fix viewport resize 2026-09-05 11:53:50 +02:00
Jörg Breitbart 039e103ed8 add painting primitives to IImageSpec, move IIP resize to late rendering 2026-09-02 21:50:25 +02:00
Jörg Breitbart c9aabf9cf5 fix demo 2026-09-01 11:35:05 +02:00
Jörg Breitbart dc1fab5d5d multiple changes:
- renderer: delete static primitives
- renderer: use device pixels for drawing and clearing
- renderer: simplify placeholder creation
- renderer: localize dimensions in rescaleCanvas
- renderer: rescale on drawing
- storage: store raw bytes as blobs
- storage: alter addImage interface adding blob and metrics data
- storage: switch from pixel to byte counting
- storage: getBlobUsage()
- Drawable as storage/renderer handle
- use metrics for all protocols
- remove extractTileAtBufferCell
- use device pixels in CSI t 14|16 reports
2026-09-01 11:16:43 +02:00
Jörg Breitbart b7ef9fa25e cleanup primitives and move to own file 2026-08-30 22:53:15 +02:00
jerch c58ea3637f Merge pull request #5879 from PerBothner/image-ext
Change image storage to use new ExtendedAttrs payload field
2026-08-30 22:11:27 +02:00
Per Bothner 3eaea160f4 Fix bad ExtendedAttrs sharing in ImageStore 2026-08-30 12:40:59 -07:00
Per Bothner c9b14056c5 Merge remote-tracking branch 'upstream/master' into image-ext
Also make faster by removing much loadCell calls.
2026-08-30 08:52:48 -07:00
jerch d3e32b344d Merge pull request #6125 from xtermjs/webp_avif_support
webp and avif support for IIP
2026-08-24 13:33:17 +02:00
jerch 0659493637 Merge branch 'master' into webp_avif_support 2026-08-24 13:27:57 +02:00
Anthony Kim 08ad9a4de9 Merge pull request #6131 from anthonykim1/anthonykim1/fix-image-text-overwrite
Restore text overwrite of image tiles
2026-08-23 19:58:25 -10:00
Anthony Kim fbaf28af5c restore text overwrite of image tiles 2026-08-23 22:48:22 -07:00
Anthony Kim 8938fe3785 Merge pull request #6128 from anthonykim1/anthonykim1/fix-pattern-glyph-owner-document
Fix pattern custom glyph rendering across documents
2026-08-23 17:35:52 -10:00
Anthony Kim 163104e14f Fix pattern custom glyph rendering across documents 2026-08-23 20:12:13 -07:00
Jörg Breitbart 9db3a4beeb update addon docs 2026-08-22 11:27:37 +02:00
Jörg Breitbart 3558626911 add demo images 2026-08-22 11:15:32 +02:00
Jörg Breitbart a783fc1817 test images for WEBP and AVIF 2026-08-22 10:52:45 +02:00
Jörg Breitbart 90fad0f277 make linter happy 2026-08-21 02:08:45 +02:00
Jörg Breitbart 4e41215a87 webp and avif support 2026-08-21 02:02:24 +02:00
jerch 6a49e1aa61 Merge pull request #6122 from xtermjs/fix_handleBufferLinesTrim
perf: optimize DecorationService._handleBufferLinesTrim
2026-08-20 18:13:02 +02:00
jerch f039ba3fba Merge branch 'master' into fix_handleBufferLinesTrim 2026-08-20 18:01:07 +02:00
jerch 116a09617e Merge pull request #6114 from xtermjs/fix_bufferline
bufferline perf fixes
2026-08-20 18:00:12 +02:00
Jörg Breitbart bc62843844 optimize Event.fire 2026-08-20 16:32:41 +02:00
Jörg Breitbart aca98b8a5e silence noisy code for most trim events 2026-08-20 16:32:14 +02:00
Jörg Breitbart 409c931093 remove any type 2026-08-20 12:50:14 +02:00
Jörg Breitbart 4c8963a487 remove cache invalidation from clone 2026-08-19 13:55:26 +02:00
Jörg Breitbart c480898d0d fix naming typo 2026-08-19 01:11:53 +02:00
Jörg Breitbart c44a913c2d faster extended resetting in loadCell 2026-08-18 23:50:15 +02:00
Jörg Breitbart 4708890a27 simpler string cache on bufferline 2026-08-18 22:34:57 +02:00
Jörg Breitbart fa3821adb9 optimize copyFrom and clone for blank lines 2026-08-18 16:36:57 +02:00
Per Bothner e69a8b6240 Merge from upstream. 2026-08-14 10:37:20 -07:00
Per Bothner 9f64663bd0 Merge branch 'master' into image-ext 2026-08-14 07:48:16 -07:00
29a7384233 ci: scope GitHub Actions permissions to least privilege (#6028)
Co-authored-by: Ankur Jangid <ankurjangid@Ankurs-MacBook-Air.local>
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-10 11:39:43 +00:00
Taran d6cfe3db5d Fix viewport staying pinned to the top after ESC[3J clears the scrollback (#6081) 2026-08-10 11:38:58 +00:00
8c9b9fdb9b ci: restrict release workflow permissions (#6095)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-08-10 04:34:45 -07:00
dependabot[bot]andPretaSOFT 6ccafd9779 build(deps): Bump ws (#6094)
Bumps [ws](https://github.com/websockets/ws) to 7.5.13 and updates ancestor dependency . These dependencies need to be updated together.


Updates `ws` from 7.5.10 to 7.5.13
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.10...7.5.13)

Updates `ws` from 8.20.1 to 8.21.3
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/7.5.10...7.5.13)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 7.5.13
  dependency-type: indirect
- dependency-name: ws
  dependency-version: 8.21.3
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 06:16:25 -07:00
dependabot[bot]andPretaSOFT b5222f8010 build(deps): Bump brace-expansion (#6093)
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 5.0.6 to 5.0.9
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.9)

Updates `brace-expansion` from 2.1.0 to 2.1.4
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 2.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 06:02:19 -07:00
Simon Lamon e2ff222c4f Apply autofixes from Zizmor (#6021) 2026-08-09 06:01:58 -07:00
dependabot[bot]andPretaSOFT 1b822cb814 build(deps): Bump shell-quote and concurrently (#6076)
Bumps [shell-quote](https://github.com/ljharb/shell-quote) to 1.9.0 and updates ancestor dependency [concurrently](https://github.com/open-cli-tools/concurrently). These dependencies need to be updated together.


Updates `shell-quote` from 1.8.3 to 1.9.0
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/shell-quote/compare/v1.8.3...v1.9.0)

Updates `concurrently` from 9.2.1 to 9.2.4
- [Release notes](https://github.com/open-cli-tools/concurrently/releases)
- [Commits](https://github.com/open-cli-tools/concurrently/compare/v9.2.1...v9.2.4)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.9.0
  dependency-type: indirect
- dependency-name: concurrently
  dependency-version: 9.2.4
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 06:01:11 -07:00
rufatkulandClaude Fable 5 8961a72d2d Set autocomplete="off" on the helper textarea (#6057)
Without an autocomplete hint, focusing a terminal on iOS/iPadOS Safari
pops the AutoFill accessory (passwords / credit cards / contacts) for
the hidden textarea. The sibling hints (autocorrect, autocapitalize,
spellcheck) are already set; this adds the missing one.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 05:56:52 -07:00
Graciliano Monteiro PassosandDaniel Imms 576181d93d Add OmnyShell PWA to the list of tools (#6034)
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-09 05:54:39 -07:00
kkd927andDaniel Imms eef62138a4 Add kmux to real-world uses (#6031)
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-09 05:54:04 -07:00
dependabot[bot]andPretaSOFT c793c317fd build(deps-dev): Bump fast-uri from 3.1.2 to 3.1.5 (#6091)
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 05:52:59 -07:00
dependabot[bot]andPretaSOFT a4e4e28f97 build(deps-dev): Bump undici from 7.25.0 to 7.29.0 (#6088)
Bumps [undici](https://github.com/nodejs/undici) from 7.25.0 to 7.29.0.
- [Release notes](https://github.com/nodejs/undici/releases)
- [Commits](https://github.com/nodejs/undici/compare/v7.25.0...v7.29.0)

---
updated-dependencies:
- dependency-name: undici
  dependency-version: 7.29.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-09 05:52:51 -07:00
Bob LeeandDaniel Imms 4452773563 Add BitFun to real-world uses (#6092)
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-09 05:52:24 -07:00
Gevik BabakhaniandDaniel Imms 7aea5197d8 Add JSVision to real-world uses (#6085)
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-09 05:50:48 -07:00
Ark0NandDaniel Imms 9f39dc2bf9 Add Codeman to real-world uses (#6082)
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-08-09 05:50:08 -07:00
KindaBrazy 330eaaf205 add "LynxHub" to Real-world uses. (#6077) 2026-08-09 05:49:33 -07:00
Anthony Kim 904ae93526 Merge pull request #6055 from heudev/fix-shared-atlas-clear-invalidation
Fix stale rendering after a shared texture atlas is cleared
2026-07-26 21:24:07 -07:00
Anthony Kim 699f5537b0 Merge pull request #6056 from anthonykim1/anthonykim1/webgl-atlas-tui-stress-dual-coverage
Add WebGL atlas alternate-buffer stress test and refine the manual rendering demo
2026-07-19 16:26:06 -07:00
Anthony Kim abce6f1c62 call scroll output 2026-07-17 11:36:15 -07:00
Anthony Kim 4d93815c36 Improve types in webglatlasstress.test.ts 2026-07-17 11:31:12 -07:00
Anthony Kim 5b00193876 adjust button style 2026-07-17 11:17:30 -07:00
Enes Uysal 0b1c0b5c5a Fix stale rendering after a shared texture atlas is cleared
TextureAtlas.clearTexture() wipes every page and both cache maps but never
bumped _pageLayoutVersion, so it silently broke the invalidation contract that
#6042 introduced for shared atlases.

The atlas is shared by every terminal with an equal config (CharAtlasCache),
but WebglRenderer.clearTextureAtlas() only clears the *calling* renderer's
model. Every other owner keeps vertex data pointing into the rows that were
just wiped, and GlyphRenderer.beginFrame() has no way to notice, so those
terminals keep drawing from stale texture coords until something else happens
to invalidate them. Once the clearing terminal rasterizes new glyphs into the
freed rows, the siblings render whatever landed there instead of their own
text.

Embedders hit this whenever they force a redraw on one terminal while others
are alive; VS Code calls Terminal.clearTextureAtlas() on OS resume, which is
why a stale terminal there stays garbled even after opening a new one (the new
terminal re-acquires the same cached atlas) and only recovers once the whole
panel is closed and the last owner drops the atlas.

Bump _pageLayoutVersion on clear so sibling renderers rebuild their models on
their next frame, matching what page merges and evictions already do.

The added test fails on master (terminal B's row renders terminal A's flood
glyphs) and passes with the fix.
2026-07-16 18:38:24 +03:00
Anthony Kim ce21694856 Merge pull request #6043 from anthonykim1/anthonykim1/webgl-atlas-page-cap-eviction
Prevent WebGL atlas pages from exceeding texture capacity
2026-07-15 09:34:06 -07:00
Anthony Kim 2ea67f9ea8 Clean up WebGL atlas overflow coverage 2026-07-13 11:49:25 -07:00
Anthony Kim 2ae6e3a603 Useful comments. 2026-07-13 11:23:39 -07:00
Anthony Kim 06ce754fcc Don't mess with formatting 2026-07-13 11:10:08 -07:00
Anthony Kim 6791a0abea Log to measure eviction time 2026-07-13 10:58:05 -07:00
Anthony Kim 5501e931f7 Merge branch 'master' into anthonykim1/webgl-atlas-page-cap-eviction 2026-07-13 10:20:44 -07:00
Anthony Kim b1aee19ac6 Merge pull request #6042 from anthonykim1/anthonykim1/webgl-shared-atlas-pages-version
Fix stale rendering after shared texture atlas page merges
2026-07-13 10:16:29 -07:00
Anthony Kim 2b982bc39c Prevent WebGL atlas page overflow 2026-07-09 15:54:33 -07:00
Anthony Kim 8237004d5c Add WebGL atlas page overflow repros 2026-07-09 15:51:27 -07:00
Anthony Kim 9559005bbc wroding 2026-07-09 11:39:38 -07:00
Anthony Kim 46556eb6cf Rename to pageLayoutVersion 2026-07-09 11:32:23 -07:00
Anthony Kim 64ecce4f25 Do a better job with types in testing 2026-07-09 11:18:29 -07:00
Anthony Kim 77bf56066c wordings 2026-07-09 11:00:31 -07:00
Anthony Kim 6fe6fe1af3 Fix shared atlas page invalidation per renderer 2026-07-04 14:44:23 -07:00
Anthony Kim 780cbef3cf Add shared atlas page merge garble repro 2026-07-04 14:43:52 -07:00
Anthony Kim 8aab310366 Merge pull request #6019 from anthonykim1/anthonykim1/fix-terminal-window-mouse-listeners
Use current document for mouse listeners
2026-06-28 13:56:22 -07:00
Anthony Kim 9f232d981b destructure doc from target like the old days 2026-06-16 19:53:03 -07:00
Anthony Kim 1efd80cd17 Dont forget target.document 2026-06-16 19:44:27 -07:00
Anthony Kim 1701dcef90 use disposables for transient mouse listeners 2026-06-16 19:35:29 -07:00
Anthony Kim 55dbe3b65e use current document for mouse listeners 2026-06-16 19:21:19 -07:00
Per Bothner fed1138b85 Merge branch 'master' into image-ext 2026-06-13 06:24:20 -07:00
Paul Horn 53a98a720a Add Cate to real-world uses (#6013) 2026-06-13 03:12:27 -07:00
dependabot[bot]andPretaSOFT b69ae4b3c9 build(deps-dev): Bump esbuild from 0.28.0 to 0.28.1 (#6016)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.28.0 to 0.28.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-version: 0.28.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-13 03:09:12 -07:00
Per Bothner 092b07da86 Merge remote-tracking branch 'upstream' into image-ext 2026-06-07 13:47:27 -07:00
3cdbc77386 fix(addon-search): advance highlight scan by buffer match size (#5994)
* fix(addon-search): advance highlight scan by buffer match size

_highlightAllMatches() stepped to the next find() using term.length
and col + 1 when staying on the same row. Search results expose size
in buffer columns (wide glyphs, wrapped matches), which select() and
decorations already use.

Advance with col + size and wrap row/col when crossing the terminal
width so highlight-all does not skip, duplicate, or stop early on
non-ASCII matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(addon-search): regression for highlight-all wide char advance by size

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* test(addon-search): move highlight-all regression to integration tests

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 11:12:04 -07:00
f57b8e1534 Update GitHub Actions to Node 24 compatible versions (#6007)
Upgrade actions/checkout, setup-node, upload-artifact, download-artifact,
and codeql-action to their latest major versions that run on Node 24.

Replace NathanFirmo/wait-for-other-job with native needs: build for
test-unit and test-integration jobs.

Fixes #6006

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:58:42 +00:00
c8f11d63b5 fix(terminal): always run clear() at cursor home (#5992)
Remove early return when ybase===0 && y===0 so clear() always
clears markers, resets buffer lines, and refills the viewport.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:43:22 +00:00
3d929a1db0 fix(selection): drag-scroll end row and trim start clamp (#5993)
* fix(selection): drag-scroll end row matches last visible line

Use ydisp + rows - 1 for selectionEnd when scrolling down during drag,
consistent with finalSelectionEnd for select-all.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* fix(selection): reset start to origin when start row is trimmed

When handleTrim removes the selection start row, clamp to [0,0] and
refresh; do not keep a stale column on row 0.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:18:47 +00:00
2fe3fd13a1 fix(addon-webgl): skip missing buffer lines in _updateModel (#5991)
* fix(addon-webgl): skip missing buffer lines in _updateModel

_updateModel force-unwrapped buffer.lines.get(row), which can be
undefined during trim or resize. That threw and left stale glyphs on
rows below the gap.

Clear the WebGL model row and glyph cells, reset blink state, and
continue with remaining viewport rows, matching DomRenderer and
ImageStorage handling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(addon-webgl): extract _nullModelCell helper

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 09:15:09 -07:00
Daniel ImmsandCursor f33f502267 fix(DomRenderer): resilience when buffer lines are missing (#5998)
* fix(DomRenderer): skip missing buffer lines during row render

renderRows and link underline refresh stopped updating when any
viewport row lacked buffer data, leaving stale DOM on rows below.
Match ImageStorage.render behavior: clear the row element when
present and continue with remaining rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(DomRenderer): clear blink state when skipping missing lines

When a viewport row has no buffer line, renderRows clears the row DOM
and continues but did not reset _rowHasBlinkingCells for that row.
That could leave the text blink manager thinking blinking cells remain
after the row was emptied.

Clear blink state for the row when skipping a missing buffer line,
including during link underline refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 09:09:32 -07:00
jerch 7c24c58007 fix non-ascii clipboard handling (#6002)
* fix non-ascii clipboard handling

* rename type helpers

* make linter happy
2026-06-06 09:07:03 -07:00
jerch 596634ac41 fix #6003 (#6004) 2026-06-06 09:02:43 -07:00
jerch 0c8271db49 fix atlas callstack exception (#6001)
* fix atlas callstack exception

* remove spread operator, integrate stress test into demo
2026-06-06 09:01:49 -07:00
Daniel ImmsandCursor f3f4dcaf9f fix(CharacterJoiner): bound join ranges to trimmed line length (#5990)
Character joiner scanning already iterated cells with
getTrimmedLength(), but trailing attribute runs and
_stringRangesToCellRanges used buffer cols. On short lines that
could run joiners on a single trailing cell and extend open ranges
through empty tail columns, affecting ligature/join rendering.

Use trimmed length consistently for trailing-run detection, the
cell-range walk, and the final range end column.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 07:05:19 -07:00
64718444e2 fix(webgl): avoid glyph atlas mipmaps (#5987)
Co-authored-by: Will Hampson <whamp@ggl.slmail.me>
Co-authored-by: jerch <jerch@rockborn.de>
2026-06-03 06:00:44 -07:00
ea75b99e2b fix(deps): override serialize-javascript to ^7.0.3 (#5989)
Mocha 11.7.x pulls in serialize-javascript 6.0.2, which is affected by
code injection via RegExp.flags and Date.toISOString (CVE incomplete fix).
Force the patched 7.x release via npm overrides.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-02 13:34:19 +00:00
Daniel ImmsandCursor 564113e053 Add unit tests for parser reset, buffer scroll, ConPTY, and search (#5988)
* Add InputHandler unit tests for ISO-2022 charset mapping

InputHandler unit tests always used MockCharsetService, so regressions in
print()'s ASCII remapping, G0/G1 designation (ESC ( ) * +), and SO/SI
level switching were never caught at unit level.

Add an ISO-2022 character sets suite wired to real CharsetService and
BufferService that verifies DEC line drawing on G0 (ESC ( 0), G1 after
ESC ) 0 + SO, and charset/glevel restoration via ESC 7 / ESC 8 before
printing.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scroll unit tests for isUserScrolling

BufferService.scroll() adjusts ybase and ydisp differently depending on
whether the circular buffer is full and whether isUserScrolling is set,
but no unit test asserted those branches. InputHandler and other callers
use MockBufferService.scroll, which throws, so real scroll viewport logic
was only exercised indirectly.

Add BufferService.test.ts covering full-buffer scroll with the user
scrolled up (ydisp decrements), partial-buffer scroll while scrolled up
(ydisp unchanged), and full-buffer scroll at the viewport bottom
(ydisp tracks ybase).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit tests for DECOM cursor positioning

CUP with DEC origin mode (DECOM) offsets rows by the scroll region top
and clamps within DECSTBM margins, but unit tests only toggled the mode
for save/restore. Playwright covered the combination; headless unit tests
did not.

Add a cursor positioning test matching the Playwright DECOM case: with
margins and origin on, CUP places the cursor relative to scrollTop, clamps
to the margin, and uses absolute rows when origin is off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add WindowsMode unit tests for wrap heuristic edges

ConPTY does not set isWrapped on soft-wrapped lines; updateWindowsModeWrappedState
infers wrapping from the last cell of the previous row. Browser Terminal tests
exercise this only through full writes, with no direct coverage of the helper
or negative cases (trailing space or null).

Add WindowsMode.test.ts asserting isWrapped is set for a full non-space line,
cleared for trailing whitespace, and cleared when the previous line ends empty.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add OscParser.reset unit test for mid-payload abort

OscParser.reset() must call end(false) on active handlers when aborted
during PAYLOAD, but no test invoked reset() directly. EscapeSequenceParser
tests mock OSC, so production reset/cleanup was unverified at unit level.

Add a reset describe block asserting handlers receive END false after a
partial payload, then a subsequent OSC sequence completes normally.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DcsParser.reset unit test for mid-payload abort

DcsParser.reset() must call unhook(false) on hooked handlers when aborted
mid-DCS, matching OscParser's end(false) cleanup. No DcsParser test called
reset() directly, leaving the abort path unverified at unit level.

Add a reset describe block asserting UNHOOK false after partial put, then a
subsequent DCS sequence completes with UNHOOK true.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler print tests for grapheme joining

print() uses UnicodeService.shouldJoin to merge combining marks, but
InputHandler tests never exercised that path. The U+200B infinite-loop
regression test used mocks and had no assertions, so it would pass even
if print() misbehaved.

Add tests for e + combining acute in one parse and across two parses.
Strengthen the U+200B regression test to use BufferService and assert the
buffer does not grow spurious rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add CharsetService unit tests for glevel and designation

CharsetService only updates the active charset when setgCharset targets the
current glevel; most InputHandler tests use MockCharsetService instead. A
regression in setgLevel or inactive-slot designation would not be caught
without direct service tests.

Add CharsetService.test.ts covering inactive designation, setgLevel
activation, active-slot updates, and reset clearing state.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add reflowCursorLine unit tests for ConPTY resize behavior

reflowCursorLine lets resize reflow wrapped lines that contain the cursor
(needed when ConPTY does not). No test referenced the option or
reflowLargerGetLinesToRemove's cursor skip branch.

Add BufferReflow tests for skip vs reflow when the cursor is inside a
wrapped block, and Buffer.resize tests showing merge on widen only when
reflowCursorLine is enabled.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add EscapeSequenceParser reset test for async pause recovery

reset() sets ParserStackType.RESET so the next parse continues in GROUND
at chunkPos + 1 without resuming a paused async handler. Existing async
tests only verified reset lifts FAIL after improper continuation, not
mid-chunk recovery with trailing printable data.

Add a test that pauses on async CSI, calls reset(), then continues parsing
and asserts XY is printed once and the stack returns to NONE.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Buffer.resize unit test for ConPTY row growth

When rows increase on Windows ConPTY, Buffer.resize appends lines without
decrementing ybase or ydisp so the viewport stays stable while ConPTY
repaints. Buffer tests covered the non-Windows scroll-up path but never
passed windowsPty into options.

Add a ConPTY row-growth test asserting ybase, ydisp, and line count when
windowsPty is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add Buffer reflow test for ConPTY buildNumber threshold

Buffer disables reflow on ConPTY when buildNumber is below 21376, but no
unit test asserted that gate. Column shrink without reflow leaves wrapped
content on a single line instead of splitting across rows.

Add a test comparing shrink behavior at build 21375 versus 21376, plus
retain unwrap-on-widen coverage for supported ConPTY builds.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add ApcParser.reset unit test for mid-payload abort

ApcParser.reset() calls end(false) on active handlers when aborted mid-APC,
matching OscParser and DcsParser, but no ApcParser test invoked reset()
directly.

Add a reset describe block asserting END false after partial put and a
clean subsequent APC sequence.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit test for REP with grapheme clusters

repeatPrecedingCharacter uses parser.precedingJoinState and reprints the
full cluster from the buffer, but only Playwright covered e + combining
acute with CSI b. A regression could break REP for joined sequences without
failing unit tests.

Add a print-suite test asserting CSI 2 b triples e + acute in one parse.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add InputHandler unit tests for REP no-op cases

repeatPrecedingCharacter returns early when parser.precedingJoinState is
zero, including after another escape sequence clears join state. Only the
positive grapheme REP case was unit tested.

Add tests for bare CSI b and for REP following SGR so regressions cannot
spuriously duplicate buffer content.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DecorationManager unit test for wrapped search matches

DecorationManager splits matches across wrapped rows with per-segment x
and width, but only SearchEngine had unit coverage; decoration geometry
was left to Playwright.

Add DecorationManager.test.ts using a real Terminal, a wrapped-buffer
match from SearchEngine, and assertions on registerDecoration options
for both row segments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scroll unit test for DECSTBM margins

When scrollTop is non-zero, scroll() shifts lines inside the margin with
shiftElements instead of pushing scrollback. Existing BufferService tests
only covered scrollTop === 0.

Add a test with margins on rows 1–3 that verifies rows outside the region
stay fixed and lines inside the region scroll correctly.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add BufferService.scrollLines unit tests

scrollLines adjusts ydisp and toggles isUserScrolling for viewport
scrolling, but only scroll() had unit coverage. Regressions in the
user-scroll flag or ydisp clamping would affect the whole viewport.

Add tests for scrolling up, clamping at ydisp 0, and clearing
isUserScrolling when reaching the buffer bottom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Add DecorationManager overview ruler dedup unit test

Multiple highlight matches on the same buffer line should only register
one overview ruler marker; later matches on that line omit ruler options.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Assert overview ruler on each line of wrapped search match

Wrapped highlight decorations span two buffer lines; each segment
should register overview ruler options unlike same-line dedup.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 12:41:54 +00:00
b3de9fa6db fix(buffer): clear stale attrs and combined data in BufferLine (#5972)
* fix(buffer): bound combined-cell copy in copyCellsFrom

When copying a column range, combined-string metadata was copied for
every source column >= srcCol with no upper limit. Partial copies could
pull _combined entries from outside [srcCol, srcCol + length) into the
destination and corrupt cells beyond the copied span.

Only copy combined keys within the same range as the cell data loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale _combined entry in BufferLine.setCell

setCell updated combined data when IS_COMBINED_MASK was set but left
orphaned _combined[index] entries when writing a non-combined cell.
Delete the map entry in that case so later combined copies and lookups
stay aligned with cell content flags.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale extended attrs in BufferLine.setCell

setCell stored extended attributes when HAS_EXTENDED was set but did
not remove orphaned _extendedAttrs[index] when overwriting with a
cell without extended background data. Delete the entry in that case,
matching the combined-cell cleanup added for setCell earlier.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear dest extended attrs in copyCellsFrom

When copying a column range, extended attributes were copied from
source cells with HAS_EXTENDED but stale _extendedAttrs on the
destination were left for cells without extended data. Delete dest
entries in that case, matching setCell behavior and the bounded
_combined copy loop.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear extended attrs in setCellFromCodepoint

The input-handler fast path set extended data when HAS_EXTENDED was
present but left orphaned _extendedAttrs entries when overwriting
with non-extended cells. Delete the map entry in that case, matching
setCell and copyCellsFrom.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(buffer): clear stale combined data in setCellFromCodepoint

When the input handler overwrites a cell that previously held a
grapheme cluster (IS_COMBINED_MASK), setCellFromCodepoint updated
_data and _extendedAttrs but left this._combined[index] intact.
setCell already deletes _combined for non-combined cells; this path
did not, so getString and renderers could show the old cluster string
after a normal codepoint write.

Add delete this._combined[index] at the start of setCellFromCodepoint,
matching setCell's non-combined branch and the extended-attrs cleanup
already performed in the same method.

Co-authored-by: Cursor <cursoragent@cursor.com>

* perf(buffer): only delete stale combined/extended map entries when present

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* fix(buffer): treat combined/extended maps as flag-gated caches

_flags in _data are authoritative; sparse maps are only read when
IS_COMBINED_MASK or HAS_EXTENDED is set. setCellFromCodepoint no longer
touches _combined. loadCell clears CellData side fields when flags are off.
copyCellsFrom copies map entries per-cell from src flags only.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-02 03:04:25 +00:00
1f054097f3 Fix benchmarks with NODE_PATH and common/browser imports (#5985)
PR #5966 switched benchmark imports to ../../src/ paths that do not
resolve at runtime. Restore common/* and browser/* imports (with
benchmark-only tsconfig paths for typechecking) and NODE_PATH=./out
on benchmark npm scripts so emitted requires resolve against out/.

Fixes #5970

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 19:43:47 -07:00
Daniel Imms a250987508 Merge pull request #5984 from xtermjs/cursor/addons-functional-fixes-89d6
fix(addons): search bounds, webgl sort, web-links regex, fit height clamp
2026-06-01 19:34:14 -07:00
Daniel Imms 21c99d0574 fix(bin): script path, exit codes, and integration suite handling (#5973)
<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary

Improves reliability and correctness of `bin/` maintenance scripts used for local development and CI.

## Behavioral fixes

- **Exit codes**: `test_integration.js` exits with code 1 when `--suite` names an unknown suite; `test_unit.js` uses a non-zero fallback (`-1`) when the coverage or mocha child process has no status; `esbuild_all.mjs` exits non-zero if any parallel esbuild child fails.
- **Repo root**: `setup-full.mjs` resolves the repository root one directory above `bin/` (was incorrectly using `bin/` as root).
- **esbuild_all**: Propagates child build failures instead of always exiting 0.
- **Integration suites**: When `--suite` is omitted, all configured Playwright suites (core + addons) run; when provided, only the matching suite runs.
- **test_unit.js**: Declares the `files` filter variable so file arguments from `npm run test-unit` work without a ReferenceError.
- **test_mousemodes.js**: Uses the correct cursor-home CSI (`ESC [ H`) when clearing the menu screen.
- **Cleanup**: Removes debug logging from the test-unit coverage path; adds a missing semicolon after the suite regex match.

## Affected scripts

- `bin/test_integration.js`
- `bin/test_unit.js`
- `bin/esbuild_all.mjs`
- `bin/setup-full.mjs`
- `bin/test_mousemodes.js`

## Testing

- Syntax-checked affected scripts with `node --check`
- Verified `node bin/test_integration.js --suite=unknown` exits 1 with an error message
- `npm run lint-changes` (no TypeScript files changed)
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a href="https://cursor.com/agents/bc-94b28f5d-740c-4cb6-8cd9-40f35bc6086c"><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-94b28f5d-740c-4cb6-8cd9-40f35bc6086c"><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-06-01 19:34:13 -07:00
Daniel Imms 0b6fd2e8b2 Merge pull request #5979 from xtermjs/cursor/addon-image-readme-typos-89d6
docs(addon-image): fix README spelling and wording
2026-06-01 10:38:36 -07:00
Daniel Imms 4226e64245 Merge pull request #5982 from xtermjs/cursor/addons-docs-typos-7eb2
docs(addons): fix comment and README typos (webgl, web-links, unicode-graphemes)
2026-06-01 10:35:35 -07:00
Daniel Imms a374c6c5af Merge pull request #5980 from xtermjs/cursor/browser-functional-fixes-b5c0
fix(browser): debounce, render, mouse coords, and CoreBrowserTerminal
2026-06-01 09:23:57 -07:00
Cursor AgentandDaniel Imms 454e0749c2 fix(browser): use ??= in RenderDebouncer for lint
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 16:15:01 +00:00
Daniel Imms 59264f1f5b Merge pull request #5974 from xtermjs/cursor/parser-validation-fixes-89d6
fix(parser): Params error grammar and ESC prefix precedence
2026-06-01 09:05:28 -07:00
Cursor AgentandDaniel Imms 16ef64f8bf Merge origin/master into cursor/browser-functional-fixes-b5c0
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:59:10 +00:00
Daniel Imms a0f93cdcc4 Merge pull request #5983 from xtermjs/cursor/test-fixes-explore-e565
test: search, parser title, and attach suite fixes
2026-06-01 08:50:09 -07:00
Daniel Imms 5b911db9cf Merge branch 'master' into cursor/test-fixes-explore-e565 2026-06-01 08:45:29 -07:00
Daniel Imms 560c0ce3a1 Merge pull request #5978 from xtermjs/cursor/src-docs-typos-0755
docs(src): fix comment and JSDoc typos
2026-06-01 08:33:32 -07:00
Daniel Imms 94958c2489 Fix whitespace 2026-06-01 08:33:02 -07:00
Cursor AgentandDaniel Imms 6426573f45 docs(addon-web-links): shorten comment to satisfy max-len after typo fix
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:30:04 +00:00
Daniel Imms 5ab59506aa Merge pull request #5975 from xtermjs/cursor/contributing-typo-b5c5
docs: fix typo in CONTRIBUTING.md
2026-06-01 08:30:00 -07:00
Cursor AgentandDaniel Imms 096707d736 test: fix typos in test titles and comments
Apply test-only hunks from docs typo fixes: SharedRendererTests
duplicate word, SerializeAddon workaround comment, and mouse range
spelling in MouseService and InputHandler tests.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:18 +00:00
Cursor AgentandDaniel Imms 967d3f59e6 docs(addon-image): fix README spelling and wording
README spelling and wording fixes only; no code changes.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:08 +00:00
Daniel ImmsandCursor 9a70816dad test(addon-attach): correct Playwright suite name in AttachAddon tests
The integration test file used test.describe('Search Tests'), copied from
addon-search. That mislabels attach-addon results in Playwright reports and
makes the suite harder to filter or debug.

Rename the block to 'AttachAddon' so the describe name matches the addon
under test. No test logic changed.

Validated with: npm run build && npm run esbuild, npm run test-unit,
npm run esbuild-demo-client && npm run esbuild-demo-server &&
npm run test-integration, npm run package, npm run package-headless.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:29:07 +00:00
Daniel ImmsandCursor 8e7e4f16e5 test(parser): fix typo in EscapeSequenceParser test title
Rename the mocha case "inital states" to "initial states".

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:29:07 +00:00
Daniel ImmsandCursor a9a2a8e3c1 test(search): reject readFile errors in #2444 fixture loader
beforeAll always resolved the readFile promise with data even when
err was set, which led to calling toString on undefined. Reject the
promise on err so fixture load failures surface clearly. Use res/rej
callback names to avoid shadowing path.resolve.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:29:07 +00:00
Daniel ImmsandCursor 92a1bfcf2e test(search): send real CSI CUP in #3834 null-char case
The integration test intended to move the cursor forward with ESC [ C
but wrote the literal string \x1b[C due to double escaping. Use a
single-escaped '\x1b[CHi Hi' so the terminal receives the control
sequence and creates the null cell the scenario describes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:29:06 +00:00
Cursor AgentandDaniel Imms be38252e37 docs: fix comment typos in Constants, DomRenderer, and addons (addons only)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:04 +00:00
Cursor AgentandDaniel Imms 0b590dacd6 docs: fix comment typos in BufferReflow, Types, and addons (addons only)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:04 +00:00
Cursor AgentandDaniel Imms 1faa41eac1 docs: fix typos in typings, tests, and core comments (addons only)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:03 +00:00
Cursor AgentandDaniel Imms fa5eb33cdb fix: small comment, error string, and guard cleanups (addons only)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:02 +00:00
Cursor AgentandDaniel Imms 3494f6b5b0 docs: fix additional comment typos across renderer and selection (addons only)
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-01 15:29:02 +00:00
Daniel ImmsandCursor 58fc09ff0e docs(webgl): remove duplicate word in context lost README
The webglcontextlost section repeated "fired" ("fires the ... event
fired on the canvas"). Drop the extra word.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:51 +00:00
Daniel ImmsandCursor a47e4905e6 docs(addon-webgl): fix comment typos in TextureAtlas and GlyphRenderer
Correct "transferred", remove duplicate "is", and use "one-off"
instead of "once of" in developer-facing comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:51 +00:00
Daniel ImmsandCursor 1252c69ccc docs(addons): fix comment typos in unicode-graphemes and web-links
Correct grapheme/handling spelling in UnicodeGraphemesAddon header and
everything/addresses typos in WebLinksAddon URL matcher comments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:51 +00:00
Daniel ImmsandCursor be3dd80171 docs(parser): fix JSDoc typos in IFunctionIdentifier
Correct "representation" and "registration" spelling in the comment
describing how handler identifiers are encoded for EscapeSequenceParser.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor 2db34c4955 docs(common): fix main thread spelling in TaskQueue comment
Change "mainthread" to "main thread" in the workload scheduling note.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor 8854430df4 docs(parser): fix sub-parameter limit name in Params JSDoc
addSubParam documents a cap on stored sub parameters; the limit is
enforced via maxSubParamsLength, not subParamsLength (the current count).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor 26e771d075 docs(common): fix grammar in Unicode and platform comments
Use "a Unicode" instead of "an Unicode" on IUnicodeService.register,
and "user's platform" instead of "users platform" above the platform
detection exports.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor fd13893687 docs(buffer): fix possessive typo in Windows conpty comment
Change "it's view" to "its view" in the comment explaining why new
rows are appended on Windows when using conpty.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor 5ef8001413 docs(common): fix activeVersion onChange JSDoc wording
The onChange event comment said "activate version changed" while the
property is activeVersion. Clarify that the event fires when the active
Unicode version changes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor e2d1420ff7 docs(common): fix throughput typo in WriteBuffer comment
Correct "throughtput" to "throughput" in the FPS vs throughput tradeoff
note inside WriteBuffer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor cf1484d7fd docs(browser): fix incomplete and typo comments in Mouse.ts
getCoords had a truncated comment ("if there are no valid") and a typo
("addition" vs "additional") in the selection precision note. Complete
the char-size guard comment and fix the selection wording.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:36 +00:00
Daniel ImmsandCursor e12f007597 docs(buffer): fix Content enum names in Constants JSDoc
The Content bitmask JSDoc examples used camelCase names that do not
exist on the enum (codepointMask, isCombined, hasContent, widthMask,
widthShift). Update examples to match the real members:
CODEPOINT_MASK, IS_COMBINED_MASK, HAS_CONTENT_MASK, WIDTH_MASK, and
WIDTH_SHIFT as used throughout BufferLine and CellData.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:35 +00:00
Daniel ImmsandCursor 3a4f9e235d docs: fix "transferred" spelling in CONTRIBUTING.md
Correct typo in the note about moving support questions to
GitHub discussions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:28 +00:00
Daniel ImmsandCursor 7b05c8a1b2 docs: fix comment and JSDoc spelling typos
Correct "occurred" in IMouseEvent and keydown JSDoc, remove duplicate
"This" in EscapeSequenceParser ZDM comment, use "elapsed" instead of
"lapsed" in TimeBasedDebouncer, and fix "event" in Keyboard comment.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:14 +00:00
Daniel ImmsandCursor b45167e00b docs: fix comment typos in ThemeService and Constants
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:14 +00:00
Daniel ImmsandCursor bc42be2074 docs(common): correct ST sequence metadata and comment typos
- EscapeSequenceParser: ESC ST is ESC backslash (0x1B 0x5C), not ESC
  plus a double-quote character; align @vt metadata with C1 ST on the
  following line.
- Color.contrastRatio: document l2 as the second luminance argument.
- Marker.dispose: fix "change" -> "chance" in dispose listener comment.
- Buffer.clear: fix possessive "it's" -> "its" in JSDoc.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:14 +00:00
Daniel ImmsandCursor bc26cc16c1 fix(browser): place imports before module const in TimeBasedDebouncer
RENDER_DEBOUNCE_THRESHOLD_MS was declared above the import, which
breaks ES module conventions and can confuse tooling. Move the import
to the top of the module after the license header.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:07 +00:00
Daniel ImmsandCursor 80468eeb4c fix(browser): clear idle blink timer when handle is 0
_clearIdleTimer() used truthiness on _idleTimeout, so a setTimeout
handle of 0 would not be cleared. Compare against undefined, consistent
with RenderDebouncer rAF handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:07 +00:00
Daniel ImmsandCursor 69b11a1a48 fix(browser): treat rAF id 0 as scheduled in RenderDebouncer
dispose(), addRefreshCallback(), and refresh() used truthiness on
_animationFrame, so a requestAnimationFrame handle of 0 would not be
cancelled or deduplicated. Compare against undefined instead, matching
other timer checks in the browser layer.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:07 +00:00
Daniel ImmsandCursor 98d7c25bd5 fix(browser): cancel trailing debounce timeout on immediate refresh
TimeBasedDebouncer can schedule a trailing setTimeout when refresh
requests are throttled. If the debounce window later elapses and an
immediate refresh runs, that pending timeout was left armed and could
fire afterward, causing a duplicate _innerRefresh (e.g. extra live
region updates for screen readers).

Clear any pending timeout and reset _additionalRefreshRequested before
running the immediate refresh path.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:07 +00:00
Daniel ImmsandCursor 6d1f12cb7b fix(web-links): avoid duplicate g flag when cloning url regex
LinkComputer rebuilt the search RegExp with (regex.flags || '') + 'g',
which throws if the caller already passed a global regex. Only append
g when it is not already present in regex.flags.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:04 +00:00
Daniel ImmsandCursor 4500485e79 fix(parser): validate ESC prefix byte range with correct precedence
_identifier() checked custom prefix bytes with
`res && 0x3c > res || res > 0x3f`, which parsed as
`(res && 0x3c > res) || (res > 0x3f)` because && binds tighter
than ||. A prefix of NUL (0x00) skipped both clauses and was
accepted even though only 0x3c..0x3f are valid.

Use an explicit range test: res < 0x3c || res > 0x3f. Mirror the
same fix in ApcParser and DcsParser test identifier helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:04 +00:00
Daniel ImmsandCursor b4bd92d62c fix(webgl): use stable numeric sort for merging texture pages
The merging-pages index sort used (a, b) => a > b ? 1 : -1, which
returns -1 when a === b and violates the comparator contract. Use
(a, b) => a - b for ascending numeric order.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:04 +00:00
Daniel ImmsandCursor 1a01e9634f fix(parser): use correct grammar in Params validation errors
Replace "values lesser than -1" with "values less than -1" in
addParam and addSubParam throw messages. "Lesser" is not standard
English for numeric comparisons; tests assert the exact message.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:04 +00:00
Daniel ImmsandCursor e35a9c1e90 fix(addon-search): reject startCol equal to terminal column count
Valid column indices are 0 through cols - 1. The previous check used
startCol > cols, so on an 80-column terminal column 80 was accepted while
column 100 correctly threw. Use >= so the out-of-range guard matches the
documented error for invalid search positions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:28:04 +00:00
Daniel ImmsandCursor 1dd113b38a fix(browser): reset TimeBasedDebouncer state on dispose
dispose() cleared the pending timeout but left _refreshTimeoutID and
_additionalRefreshRequested set. After dispose, a stale timeout id or
trailing-refresh flag could confuse a new instance or tests. Match
RenderDebouncer by clearing the timeout id and reset the trailing flag.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:27:54 +00:00
Daniel ImmsandCursor 536eeb4db0 refactor(browser): remove unreachable coords guard in getCoords
getCoordsRelativeToElement always returns a two-element tuple, so the
array is always truthy and if (!coords) could never run. hasValidCharSize
already handles the only early-exit path before coordinates are computed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:27:54 +00:00
Daniel ImmsandCursor 3f3f6abfb9 fix(browser): parenthesize document fallback for CoreBrowserService
Nullish coalescing binds tighter than the conditional operator, so the
previous expression was parsed as (this._document ?? windowDefined) ?
window.document : null. When this._document was set (e.g. parent
ownerDocument or documentOverride), the ternary still returned
window.document instead of the configured document.

CoreBrowserService.mainDocument is used by AccessibilityManager, viewport
styles, and decoration renderers; CharSizeService already received
this._document directly. Add parentheses so the fallback applies only when
_document is nullish.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:27:54 +00:00
Daniel ImmsandCursor c961408bf7 fix(browser): use startY in horizontalDirection row helper call
horizontalDirection called moveToRequestedRow with targetX as the first
argument, but that parameter is the starting row (startY). resetStartingRow
and moveToRequestedCol already pass startY correctly; only this copy-paste
mistake passed a column index where a row was expected.

That could miscompute wrapped-row offsets and emit the wrong horizontal
arrow key sequence when building selection/cursor movement strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:27:54 +00:00
Daniel Imms 6fd6003d6c Merge pull request #5971 from xtermjs/cursor/parseint-radix-10-89d6
fix: pass radix 10 to parseInt across core and addons
2026-06-01 08:12:17 -07:00
Daniel ImmsandCursor d407bf561f fix(webgl): pass radix 10 for SVG arc flag parsing
CustomGlyphRasterizer parsed large-arc and sweep flags with parseInt
without a radix when drawing SVG arc paths. Use base 10 for 0/1 flags.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:58 +00:00
Daniel ImmsandCursor a1a8d1abae fix(image): pass radix 10 when parsing kitty graphics numeric keys
KittyGraphicsTypes parsed numeric control values with parseInt
without a radix. Use base 10 explicitly for format, id, dimensions,
and offset fields.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:58 +00:00
Daniel ImmsandCursor f34d0a255c fix(image): pass radix 10 in IIPHandler dimension parsing
_dim() parsed percent, pixel, and cell dimension strings with
parseInt without an explicit radix. Use base 10 for all three paths,
consistent with other numeric parsing fixes in the repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:58 +00:00
Daniel ImmsandCursor a2cd1ab29c fix(fit): parse terminal padding with radix 10 and zero fallback
proposeDimensions summed padding-top/bottom/left/right via bare
parseInt, so non-numeric padding values produced NaN and broke
available width/height. Use radix 10 and || 0 on each side, consistent
with parent element size parsing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:58 +00:00
Daniel ImmsandCursor 99550840d2 fix(fit): add radix and zero fallback for parent width parsing
Align parent width parsing with the height fix: parseInt with radix
10 and || 0 inside Math.max so non-numeric CSS width does not yield
NaN cols in proposeDimensions.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:57 +00:00
Daniel ImmsandCursor b0f6aed4a1 fix(common): pass radix 10 when parsing OSC color indices
InputHandler parsed palette indices from OSC color sequences with
parseInt without a radix after validating digits-only slots. Use
base 10 for set/report and restore color index parsing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:43 +00:00
Daniel ImmsandCursor 878e3fc52d fix(browser): pass radix 10 when parsing Chrome major version
Wheel event DPR factoring parses Chrome/(\d+) with parseInt without a
radix. Add base 10 explicitly, matching getSafariVersion and other
version parsing in the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:43 +00:00
Daniel ImmsandCursor f04679a42e fix(common): pass radix 10 when parsing rgb() components in Color
rgba/rgb CSS parsing used parseInt without a radix on captured digit
groups. Add base 10 explicitly, matching getSafariVersion and other
call sites in the codebase.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:43 +00:00
Daniel ImmsandCursor 6ebcf79358 fix(common): pass radix 10 to parseInt in getSafariVersion
Safari major version parsing used parseInt without an explicit radix.
Other call sites in the repo use base 10; add the radix for consistency
and to avoid octal interpretation of leading-zero strings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:43 +00:00
Daniel ImmsandCursor d4acf2fa45 fix(browser): use radix 10 for padding parseInt in Mouse coords
parseInt without a radix can mis-parse leading-zero padding values as
octal. Match AccessibilityManager and document rowCount JSDoc typo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-01 15:05:43 +00:00
Daniel Imms ddf2b7d091 Merge pull request #5969 from Tyriar/import-no-cycle-5897-2
Add oxlint import/no-cycle and break existing src/common cycles
2026-05-31 15:13:58 -07:00
Daniel ImmsandCursor 5684f95c8e Colocate circular list types in CircularList.ts
The import/no-cycle refactor had moved IInsertEvent, IDeleteEvent, and
ICircularList into Types.ts, with CircularList.ts re-exporting the event
types from there. That split the list abstraction from its implementation
and pulled Types into the circular-list dependency graph.

Define IInsertEvent, IDeleteEvent, and ICircularList in CircularList.ts
alongside the CircularList class. CircularList.ts now depends only on
Lifecycle and Event, not Types.ts.

Remove those definitions from Types.ts (and the unused Emitter import).

Update callers to import list types from CircularList:
- buffer/Types.ts (IBuffer.lines)
- DecorationService.ts (buffer line insert/delete handlers)
- browser TestUtils.test.ts (mock buffer lines)

Types.ts no longer imports or re-exports circular-list types, preserving
the acyclic Types -> buffer/CircularList boundary.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:37:01 -07:00
Daniel ImmsandCursor cd3dcc52b2 Colocate buffer types in buffer/Types.ts
The import/no-cycle refactor had moved IBuffer, IBufferSet, and related
cell/line types into src/common/Types.ts, with buffer/Types.ts only
re-exporting them. That inverted the natural module boundary and made
buffer code depend on the top-level types hub.

Define buffer-owned types in buffer/Types.ts instead:
- CharData, IExtendedAttrs, IAttributeData, ICellData, IBufferLine
- IMarker, IBuffer, IBufferSet, BufferIndex

Restore UnderlineStyle to buffer/Constants.ts (used by IExtendedAttrs)
and remove those definitions from Types.ts.

buffer/Types.ts imports only what it needs from outside the buffer layer:
ICircularList and ICharset from Types, IDisposable from Lifecycle,
IEvent from Event, and UnderlineStyle from Constants. Types.ts does not
import buffer/Types.ts, so the previous Types <-> buffer cycle stays broken.

Update imports across common, browser, headless, addons, and tests to use
common/buffer/Types for buffer types rather than common/Types.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:35:08 -07: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 ImmsandCursor 56a7195487 Add oxlint import/no-cycle and break existing src/common cycles
Enable oxlint's import/no-cycle rule (import plugin) so circular module
dependencies are caught in CI, similar to eslint-plugin-import/no-cycle
and biome no-import-cycles discussed in the issue.

Relocate shared types and dependencies to remove cycles under src/common:

- Types.ts: host IInsertEvent, IDeleteEvent, IParams, UnderlineStyle,
  IBuffer, and IBufferSet; drop imports from CircularList, buffer/Types,
  parser/Types, and Services
- CoreTerminal.ts: define and export ICoreTerminal
- ServiceRegistry.ts: define IServiceIdentifier (break Services registry loop)
- UnicodeService: stop constructing UnicodeV6; CoreTerminal registers the
  default v6 provider after service creation
- buffer/Types.ts and parser/Types.ts: re-export moved types for callers

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 10:08:04 -07: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
Daniel Imms 1a67b6f4b3 Merge pull request #5903 from xtermjs/cursor/space-unary-ops-5898-604c
Add space-unary-ops lint rule to prevent space after !
2026-05-26 13:01:20 -07:00
Daniel Imms afaed108e4 Merge pull request #5828 from lawrence3699/fix/addon-ligatures-prepublish-only
fix(addon-ligatures): run package build on publish
2026-05-26 12:26:53 -07:00
Daniel Imms c27d6bb462 Merge branch 'master' into fix/addon-ligatures-prepublish-only 2026-05-26 12:26:23 -07:00
Daniel Imms 9c4f0f96ca Merge pull request #5831 from juspay/fix/intersection-observer-nullify
Clear IntersectionObserver reference on dispose
2026-05-26 12:23:13 -07:00
Daniel Imms 18c370a154 Merge pull request #5813 from CodinGame/fix-wrong-window-regression
Use element window instead of global object
2026-05-26 12:18:29 -07:00
Daniel Imms 850f4ad738 Merge pull request #5872 from xtermjs/dependabot/npm_and_yarn/fast-uri-3.1.2
Bump fast-uri from 3.1.0 to 3.1.2
2026-05-26 12:15:57 -07:00
Daniel Imms b665f6259a Merge pull request #5871 from xtermjs/dependabot/npm_and_yarn/axios-1.15.2
Bump axios from 1.15.0 to 1.15.2
2026-05-26 12:15:54 -07:00
Cursor AgentandDaniel Imms d195dc9a3b Add space-unary-ops lint rule and fix SixelHandler violation
Enable @stylistic/space-unary-ops to warn on spaces after unary
operators like ! in conditions. Fix existing violation in SixelHandler.

Fixes #5898

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-26 18:03:15 +00:00
Daniel Imms 4e5994d8a2 Merge pull request #5902 from Tyriar/tyriar/cell_opt
Optimize forEachDecorationAtCell by maintaining cache against buffer line index
2026-05-26 10:52:17 -07:00
Daniel ImmsandCursor 4a0cb5ae6b Add MicrotaskTimer and use it for decoration line index sync
Introduce MicrotaskTimer as a TimeoutTimer-style helper that schedules a
single runner via queueMicrotask, with set and cancel but no cancelAndSet.

Use it in DecorationLineCache to batch insert/delete line-index updates after
marker line adjustments, replacing ad-hoc queueMicrotask scheduling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 09:52:53 -07:00
Daniel ImmsandCursor 9e72a4b6d3 Refactor decoration line index into DecorationLineCache
Extract the per-line lookup index from DecorationService into an exported
DecorationLineCache class in the same module, with DecorationService listed
first for readability.

Optimize buffer insert/delete maintenance to shift map keys in O(unique
indexed lines) instead of full re-indexing every affected decoration. Only
multi-line decorations that span the mutation point get a full re-index.

Add a direct DecorationLineCache unit test in DecorationService.test.ts.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 09:47:11 -07:00
Daniel ImmsandCursor f484584b70 Speed up decoration cell lookup with a per-line index
Refs #5176.

forEachDecorationAtCell and getDecorationsAtCell previously scanned every
registered decoration on each call. With thousands of search highlights this
becomes O(total decorations) per cell and blocks rendering during viewport
refreshes.

Maintain a Map of buffer line -> decorations that cover that line. Multi-line
decorations are indexed on every line in their span, so mixed single-line and
multi-line workloads stay fast without falling back to a full scan.

Keep the index aligned with buffer mutations:
- Register and unregister decorations in the line buckets on add/dispose.
- On trim, shift line keys synchronously (before marker handlers run).
- On insert/delete, re-index affected decorations in a microtask after marker
  line updates are applied.

Inject IBufferService so line listeners follow the active buffer. Add unit
tests for trim, dispose-on-trim, insert, and dense single-line plus multi-line
lookup. Add DecorationService.benchmark.ts to measure sparse hits and
viewport-sized grid scans with 20k decorations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-26 09:30:25 -07:00
Per Bothner 7b66cd026a Add 'type' to another import in ImageStorage.ts. 2026-05-23 16:42:09 -07:00
Per Bothner 235457ccdd Merge remote-tracking branch 'upstream/master' into image-ext 2026-05-23 16:39:23 -07:00
Per BothnerandDaniel Imms eaa3a63ddf Use 'import type' in addons/addon-image/src/ImageStorage.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2026-05-21 08:55:49 -07: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
Daniel Imms e749cb6125 Merge pull request #5891 from Tyriar/cross-env
Remove cross-env dep
2026-05-21 03:17:50 -07:00
Daniel Imms 8df996f9e7 Merge pull request #5892 from Tyriar/tyriar/deps
Remove @types/debug and trusted-types
2026-05-21 03:17:24 -07:00
Daniel Imms 51f6186352 Remove @types/debug and trusted-types 2026-05-21 03:09:41 -07:00
Daniel Imms ae5c41c518 Merge pull request #5890 from Tyriar/utf8
Remove utf8 dep
2026-05-21 03:09:04 -07:00
dependabot[bot] 17e7dafa17 Bump axios from 1.15.0 to 1.15.2
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 10:06:53 +00:00
Daniel Imms df9fb7e473 Remove cross-env dep 2026-05-21 03:06:53 -07:00
dependabot[bot] 41d9c4080f Bump fast-uri from 3.1.0 to 3.1.2
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-21 10:06:23 +00:00
Daniel Imms f100e93ba5 Remove utf8 dep 2026-05-21 03:04:16 -07:00
Daniel Imms 1d3825d341 Merge pull request #5889 from Tyriar/deepEqual
Remove deep-equal/types packages
2026-05-21 03:03:37 -07:00
Daniel Imms 57315e6cc9 Remove deep-equal/types packages 2026-05-21 02:58:43 -07:00
Daniel Imms 2be4fc68e5 Merge pull request #5884 from sotasan/addon-image-kitty-q2-suppresses-ok
addon-image: Kitty graphics q=2 should suppress OK responses too
2026-05-21 02:37:53 -07:00
Daniel Imms 66fabfc232 Merge pull request #5878 from PerBothner/demo-size-style
demo - indicate screen area size when non-auto
2026-05-21 02:36:24 -07:00
Daniel Imms 3e2a42eb29 Merge pull request #5883 from vsych/fix-webgl-atlas-merge-stale-vertices
Fix webgl rendering corruption from atlas page merges
2026-05-21 02:26:03 -07:00
Daniel Imms 6a47d09b4c Merge pull request #5888 from Tyriar/tyriar/nal-trace
Git ignore cursor nal-traces
2026-05-21 02:18:06 -07:00
Daniel Imms b36001ee7b Git ignore cursor nal-traces 2026-05-21 02:17:43 -07:00
Daniel Imms e64f25a0dd Merge pull request #5880 from ciembor/master
Add 4bit to the Real-world uses section.
2026-05-21 02:13:34 -07:00
Victor Sych 3bcb57545e Re-upload atlas textures after page merges 2026-05-21 04:59:37 +02:00
Victor Sych dc726a2ae6 Fix webgl rendering corruption from atlas page merges 2026-05-19 02:27:15 +02:00
Sōta 06e5ac7ab4 addon-image: Kitty graphics q=2 should suppress OK responses too 2026-05-18 18:26:32 +02:00
Maciej Ciemborowicz c6e3492a4a Add 4bit to the Real-world uses section. 2026-05-14 04:45:52 +02:00
Per Bothner 8883dc17a7 Change image storage to use new ExtendedAttrs payload field. 2026-05-13 11:29:00 -07:00
Per Bothner 86291b6e96 Highlight xterm-screen when size is non-auto 2026-05-12 17:53:33 -07:00
Daniel Imms c8c8b3a7fe Merge pull request #5875 from zeertzjq/kitty
Fix keys getting swallowed when enabling only "Report event types"
2026-05-12 04:23:01 -07:00
zeertzjq 50e90ddf8a Fix keys getting swallowed when enabling only "Report event types"
Per kovidgoyal/kitty#9999, enabling "Report event types" without
"Disambiguate escape codes" doesn't really make sense, and terminals may
choose whatever behavior they think makes sense. However, keys shouldn't
get swallowed (which, after #5791, happens when the !useCsiU branch
cannot handle the event), so instead make "Report event types" imply
"Disambiguate escape codes".
2026-05-12 13:41:22 +08: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
Daniel Imms 7059a07c09 Merge pull request #5874 from xtermjs/clipboard_base64
clipboard addon: various fixes
2026-05-11 06:43:58 -07:00
Daniel Imms c5c6fc2a64 Merge pull request #5791 from zeertzjq/kitty
Fix kitty keyboard "Report event types" behavior not matching spec
2026-05-11 06:42:32 -07:00
zeertzjq e0add7fe4b Move return earlier 2026-05-11 14:11:28 +08:00
zeertzjq b0f5bc2d28 Fix kitty keyboard "Report event types" behavior not matching spec
According to spec:
- The Enter, Tab and Backspace keys will not have release events unless
  "Report all keys as escape codes" is also set.
- Key events that result in text are reported as plain UTF-8 text, so
  events are not supported for them, unless the application requests
  "key report" mode.

I checked kitty's behavior. While it does report release events for keys
that result in text as CSI sequences, press and repeat events for these
are reported as plain UTF-8 text. This commit changes Xterm.js to match
this behavior.
2026-05-11 13:59:01 +08: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
Jörg Breitbart c5a11220c1 always refer to navigator.clipboard in default provider 2026-05-10 18:00:26 +02:00
Jörg Breitbart 6f55150485 make linter happy 2026-05-10 17:06:08 +02:00
Jörg Breitbart c8dddddbbb remove js-base64 dependency 2026-05-10 16:45:46 +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
Daniel Imms 6ba731d73b Merge pull request #5867 from Tyriar/inst_const_enuym
Add disposable/const enum agent instructions, convert a lot of top level consts to const enums
2026-05-02 06:59:12 -07:00
Daniel Imms 2d3909be2c Fix lint 2026-05-02 06:54:41 -07:00
Daniel Imms 1e189625fa Merge remote-tracking branch 'upstream/master' into inst_const_enuym 2026-05-02 06:49:10 -07:00
Daniel Imms 80debafa11 Convert a lot of top level consts to const enums 2026-05-02 06:44:25 -07:00
Daniel Imms 1023e40428 Merge pull request #5864 from Tyriar/tyriar/bufferline_cache
Cache translateToString calls inside BufferLine
2026-05-02 06:30:49 -07:00
Daniel Imms 5c47e6dd9a Add instructions around MutableDisposables and const enums 2026-05-02 06:30:18 -07:00
Daniel Imms 4ebb3fffe9 Ignore cache keys in serialize addon
Bit hacky, but this is due to the way we're serializing the line in the test
2026-05-02 06:21:42 -07:00
Daniel Imms 093e81b9a7 Fix build, lint 2026-05-02 06:07:41 -07:00
Daniel Imms e04dd2501f Avoid thrashing timeouts 2026-05-02 05:58:56 -07:00
Daniel Imms d911b40a47 Avoid any in test 2026-05-02 05:52:33 -07:00
Daniel Imms 3dbfa7ce3e Convert cache to Disposable 2026-05-02 05:48:08 -07:00
Daniel Imms 6287c6ddfe Cache/buffer lifecycle 2026-05-02 05:44:13 -07:00
Daniel Imms 52d4b2719c Move cache into Buffer owned object, use WeakRefs
This allows O(1) clearing of the cache, a single TTL timer and we
can avoid making BufferLine's Disposable
2026-05-02 05:32:43 -07:00
Daniel Imms 7615cd210a Store a boolean on whether the cache is trimmed 2026-05-02 04:35:18 -07:00
Daniel Imms 4d4b994c35 Support caching of trimRight 2026-05-02 04:35:18 -07:00
Daniel Imms 7fa8b7a069 Merge branch 'master' into tyriar/bufferline_cache 2026-05-02 03:56:22 -07:00
Daniel Imms a2e51e8d36 Merge pull request #5865 from Tyriar/tyriar/image
Fix integration test defaults
2026-05-01 16:22:01 -07:00
Daniel Imms f89253d24b Fix integration test defaults
Broke due to 2 recent conflicting PRs
2026-05-01 16:17:16 -07:00
Daniel Imms 7aeee95346 Merge remote-tracking branch 'upstream/master' into tyriar/bufferline_cache 2026-05-01 16:10:45 -07:00
Daniel Imms 66fcdb2583 Fix test/startCol handling 2026-05-01 14:11:14 -07:00
Daniel Imms 2a18600d18 Cache translateToString calls inside BufferLine
Fixes #5863
2026-05-01 14:06:39 -07:00
Daniel Imms 5ef4f86543 Merge pull request #5846 from xtermjs/qoi_decoder
QOI support for IIP
2026-05-01 10:28:25 -07:00
Daniel Imms 874e767b40 Merge pull request #5840 from xtermjs/fix_apc
changes in APC handling
2026-05-01 10:22:52 -07:00
Daniel Imms 605a7c6e30 Merge pull request #5844 from xtermjs/adjust_image_addon_defaults
adjust image addon defaults
2026-05-01 10:21:58 -07:00
Daniel Imms 802decb046 Merge pull request #5856 from xtermjs/fix_colorbits
fix RG colorbits in AttributeData
2026-05-01 10:21:21 -07:00
Daniel Imms 79a95dbfd8 Merge pull request #5857 from nwparker/add-orca-real-world-use
Add Orca to real-world uses
2026-05-01 10:20:18 -07:00
Daniel Imms 75ba88c51b Merge pull request #5862 from Tyriar/05-01-make_agent_instructions_harness_agnostic
Make agent instructions harness agnostic
2026-05-01 10:13:01 -07:00
Daniel Imms f9cea732f2 Make agent instructions harness agnostic 2026-05-01 10:08:04 -07: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
jerch 4dc934ce7c Merge branch 'master' into qoi_decoder 2026-04-29 15:14:29 +02:00
Jörg Breitbart b2eff5ac67 merge resize tests for PNG & QOI into one block 2026-04-29 15:09:40 +02:00
Jörg Breitbart 31c53bc9c2 IIP resizing tests for QOI 2026-04-29 14:54:57 +02:00
Jörg Breitbart 0d2975884f activate tests for webkit 2026-04-29 14:40:31 +02:00
Jörg Breitbart e8b5fd3c23 add iip resize & qoi tests 2026-04-29 14:28:02 +02:00
Neil 7abcca7e73 Add Orca to real-world uses 2026-04-28 23:57:12 -07:00
Jörg Breitbart 92339ac9dd fix #5849 2026-04-28 21:45:50 +02:00
jerch ad4a6e8cb9 Merge pull request #5802 from xtermjs/dependabot/npm_and_yarn/axios-1.15.0
Bump axios from 1.13.2 to 1.15.0
2026-04-28 21:37:24 +02:00
jerch a094340afe Merge branch 'master' into dependabot/npm_and_yarn/axios-1.15.0 2026-04-28 21:32:51 +02:00
jerch cc7e3bbbf7 Merge pull request #5855 from xtermjs/dependabot/npm_and_yarn/follow-redirects-1.16.0
Bump follow-redirects from 1.15.11 to 1.16.0
2026-04-28 21:31:52 +02:00
jerch 73d87b2c20 Merge branch 'master' into fix_apc 2026-04-28 21:26:18 +02:00
dependabot[bot] c5bf395b10 Bump follow-redirects from 1.15.11 to 1.16.0
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.11 to 1.16.0.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.11...v1.16.0)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-version: 1.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-28 19:25:56 +00:00
jerch 9d24099aa1 Merge pull request #5852 from xtermjs/fix_ci_tests
fix CI addon tests
2026-04-28 21:24:47 +02:00
Jörg Breitbart 066c227436 disable firefox webgl 2026-04-28 21:10:52 +02:00
Jörg Breitbart 32ad3a18bf disable wrong webgl tests 2026-04-28 20:42:38 +02:00
Jörg Breitbart d43b19c260 fix safari 2026-04-28 19:54:49 +02:00
Jörg Breitbart 7dd0dac859 fix safari 2026-04-28 19:49:21 +02:00
Jörg Breitbart 7e70d6ad78 fix safari 2026-04-28 19:45:06 +02:00
Jörg Breitbart 893cdd494a remove broken test condition 2026-04-28 19:36:10 +02:00
Jörg Breitbart 2794e32b3f newer safari promotes different loading state 2026-04-28 19:31:05 +02:00
Jörg Breitbart 7d69e730ca fix web-fonts tests 2026-04-28 19:24:18 +02:00
Jörg Breitbart da51d9608c add missing separator for test arguments 2026-04-28 19:07:35 +02:00
Daniel Imms 3a84bcb7ae Merge pull request #5766 from SergioChan/fix-image-renderer-desync-5691
fix(addon-image): disable desynchronized resize canvas
2026-04-26 16:17:03 -07:00
Jörg Breitbart d9082215e7 qoi support for IIP 2026-04-26 23:28:18 +02:00
Jörg Breitbart 41938b307e adjust image addon defaults 2026-04-26 16:44:22 +02:00
Jörg Breitbart 785c3cb336 fix vt feature listing 2026-04-26 14:43:03 +02:00
Jörg Breitbart ea7e958959 fix vt feature listing 2026-04-26 14:41:36 +02:00
Jörg Breitbart 915b0b81a1 fix vt feature listing 2026-04-26 14:37:30 +02:00
Jörg Breitbart 4e9abe7d96 fix vt feature listing 2026-04-26 14:34:42 +02:00
Jörg Breitbart 84aca0b912 fix ApcParser tests 2026-04-25 15:46:33 +02:00
Jörg Breitbart d82d42ef9a APC async tests 2026-04-25 15:12:45 +02:00
Jörg Breitbart 4d2f2e0d8e handler registration tests, fix mulitple immediates 2026-04-25 14:39:22 +02:00
Jörg Breitbart 56986cb7eb register APC fallback handler 2026-04-25 11:10:24 +02:00
Jörg Breitbart 4946436997 make linter happy 2026-04-24 17:35:56 +02:00
Jörg Breitbart bf83ea06e4 better read ahead condition 2026-04-24 17:29:49 +02:00
Jörg Breitbart b7e535e2ec APC perf test 2026-04-24 17:09:37 +02:00
Jörg Breitbart 8e12e69e4e basic read ahead optimization 2026-04-24 16:49:06 +02:00
Jörg Breitbart 849a9a53ca add state and transition tests 2026-04-24 16:19:02 +02:00
Jörg Breitbart ee7d84bdd3 make linter happy 2026-04-24 14:19:55 +02:00
Jörg Breitbart 9c8de633e9 multiple changes in APC handling:
- change interface to IFunctionIdentifier (API change)
- extend parser states APC_ENTRY, APC_INTERMEDIATE, APC_PASSTHROUGH
- remove APC_STRING
- change ApcParser.start to retrieve ident
- change actions APC_START and APC_PUT
2026-04-24 13:53:31 +02:00
jerch 9c5420371b Merge pull request #5838 from xtermjs/fix_terminal_benchmark
fix utf8 terminal benchmark
2026-04-24 09:24:19 +02:00
Jörg Breitbart 22778b5b49 fix utf8 terminal benchmark 2026-04-24 09:15:26 +02:00
jerch cbe59a17ae Merge pull request #5830 from homanp/perf/array-exe-handlers
perf: array-indexed execute handler lookup in hot path
2026-04-23 18:59:47 +02:00
jerch 76671cecb6 Merge branch 'master' into perf/array-exe-handlers 2026-04-23 18:50:41 +02:00
jerch a11fcd2e0b remove type hint 2026-04-23 18:50:03 +02:00
Ismail Pelaseyed b28b5d0c2b perf: use array-indexed lookup for execute handlers in hot path
Add a parallel array container for execute handlers (codes < 0x18)
alongside the existing object. Setters populate both, the EXE
fast-path reads from the array with nullish coalescing to avoid
branching. ExecuteHandlerType updated to accept optional ident param
to allow the ?? pattern with the fallback handler.
2026-04-23 18:19:10 +02:00
Sridhar Ratnakumar 685434ca84 Fix IntersectionObserver retention on dispose 2026-04-23 11:35:56 -04:00
jerch 7fe89a1c1a Merge pull request #5829 from homanp/perf/reset-zdm
refactor: add Params.resetZdm() to avoid double call on foreign object
2026-04-23 17:05:15 +02:00
Ismail Pelaseyed eaebe2ab15 refactor: add Params.resetZdm() to collapse reset+addParam(0) into single call
Replaces 10 separate reset()+addParam(0) pairs across the parser with
a single resetZdm() method that inlines both operations, avoiding two
calls to a foreign object per sequence reset.
2026-04-23 16:55:46 +02:00
jerch 32553b4135 Merge pull request #5825 from homanp/perf/csi-fast-path
perf: CSI fast-path (+42% throughput)
2026-04-23 16:16:19 +02:00
Ismail Pelaseyed 6c2465d745 docs: note EXE and CSI fast paths in parse method docs 2026-04-23 15:59:27 +02:00
jerch 5922123b9e Merge branch 'master' into perf/csi-fast-path 2026-04-23 15:53:21 +02:00
Ismail Pelaseyed eb65910c1b perf: simplify CSI fast-path - drop intermediate handling and extra length checks
Remove intermediate byte handling from the fast-path, bail to table
lookup instead. Remove redundant per-step length checks by requiring
i + 2 < length upfront and letting the for-loop guard the rest.
2026-04-23 15:35:06 +02:00
jerch f0aeb70c92 make linter happy 2026-04-23 14:39:26 +02:00
jerch 9deec7aabf make linter happy 2026-04-23 14:36:51 +02:00
Ismail Pelaseyed 18cd9858ed perf: collapse CSI state guard into single range check
Replace three !== checks (OSC_STRING, DCS_PASSTHROUGH, APC_STRING)
with one < OSC_STRING comparison. States below OSC_STRING are all
safe for the CSI fast-path; states above need the normal table path
for proper subparser abort propagation.
2026-04-23 14:09:47 +02:00
Ismail Pelaseyed 6fd7ecc812 perf: move CSI fast-path to top of main loop, add EXE fast-path, improve PRINT unrolling
Address reviewer feedback:
- CSI fast-path is now a condition at the top of the codepoint loop
  instead of inside CLEAR, avoiding table lookups for the common case
- EXE bytes < 0x18 get a fast-path before the CSI check so control
  bytes don't pay the penalty
- PRINT loop unrolling replaced with while-based version that uses
  trueish eval and spares individual length checks in the unrolled case
- Removed misplaced PRINT scan-ahead from CSI dispatch
2026-04-23 14:01:19 +02:00
lawrence3699 ff72c00219 fix(addon-ligatures): run package build on publish 2026-04-22 12:57:27 +10:00
Daniel Imms 0911688295 Merge pull request #5817 from juspay/fix/dispose-leaks
Fix dispose leaks: register CursorBlinkStateManager + _pausedResizeTask
2026-04-21 12:57:24 -07:00
Daniel Imms 62ce318a07 Merge pull request #5827 from Tyriar/or_lifecycle
Improve overview ruler lifecycle
2026-04-21 11:22:09 -07:00
Daniel Imms 08ae141ef7 Improve overview ruler lifecycle
Fixes #5826
2026-04-21 11:13:59 -07:00
Ismail PelaseyedandClaude Opus 4.6 ba7e775a17 fix: move CSI fast-path into CLEAR action to eliminate non-CSI regression
The CSI fast-path check previously ran on every byte of input, causing
3-18% regressions on ESCAPE, OSC, DCS, and other non-CSI sequences.
Moving the check into the CLEAR action handler means it only evaluates
after the state machine already identified an ESC byte. The fast-path
body is extracted into a separate _csiFast method to keep the switch
case body small for V8 optimization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 11:19:32 +02:00
Ismail Pelaseyed 487b27f512 try: scan print run after CSI fast-path dispatch 2026-04-21 09:26:07 +02:00
Ismail Pelaseyed 5d120de3d5 try: CSI fast-path - scan entire ESC[params;final without table lookups 2026-04-21 08:50:49 +02:00
slim-forge 3ab299af7f slim-forge: baseline 2026-04-21 08:38:43 +02:00
slim-forge 6e375cb588 slim-forge: initial harness 2026-04-21 08:38:27 +02:00
Sridhar Ratnakumar 7a6e724bfe Fix dispose leaks: register CursorBlinkStateManager + _pausedResizeTask
Three related disposable-registration gaps that leak xterm Terminal
instances when the host application unmounts a Terminal. Evidence +
full writeup at juspay/kolu#606.

1. addon-webgl/WebglRenderer._cursorBlinkStateManager was declared
   `= new MutableDisposable()` without the `this._register(...)`
   wrapper that every sibling MutableDisposable in the class uses.
   CursorBlinkStateManager runs a setInterval for the cursor blink
   and its `dispose()` correctly clears it — but dispose() was never
   called because the disposable wasn't registered. The live timer
   kept the renderer (and thus the Terminal) alive past host
   unmount.

2. common/TaskQueue.DebouncedIdleTask had no `dispose()` method.
   Added one that clears the internal IdleTaskQueue (cancelling any
   pending idle callback).

3. browser/services/RenderService._pausedResizeTask (a
   DebouncedIdleTask) was not registered. Wrapped with
   `this._register(...)` so RenderService.dispose propagates.

Verified in a downstream app (kolu) with a repro that toggles Focus
vs Canvas layout modes:

- Pre-fix: 24/28 disposed xterm Terminals retained past host unmount
  + forced GC.
- Post-fix: 0/28 retained.

Retention chain (before fix):

    DOMTimer → ScheduledAction → V8Function
      → CursorBlinkStateManager._renderCallback → WebglRenderer
      → Terminal
2026-04-17 13:32:01 -04:00
Loïc Mangeonjean 69ee4ad23b fix: use element window instead of global object 2026-04-13 12:51:17 +02:00
Jeffrey Chen 253d1e9beb Underline entire wrapped OSC 8 link on hover 2026-04-12 20:06:43 -07:00
dependabot[bot] 4b77e601cd Bump axios from 1.13.2 to 1.15.0
Bumps [axios](https://github.com/axios/axios) from 1.13.2 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.2...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-12 08:49:48 +00:00
Anthony Kim 34e0179355 Merge pull request #5794 from anthonykim1/anthonykim1/optionFBKitty
Fix macOS Option+key sending wrong codepoint in Kitty keyboard protocol
2026-04-05 22:53:54 -07:00
Anthony Kim bd722e7b07 keep things clean 2026-04-03 09:46:07 -07:00
Anthony Kim cccdd447a0 No need to mention vscode link 2026-04-03 09:45:01 -07:00
Anthony Kim 446bcb1f75 Fix option b and f 2026-04-03 09:26:32 -07:00
Anthony Kim 4316263457 Merge pull request #5790 from xtermjs/anthonykim1/closeKittyKeyboardGap
Do not report lock key presses without REPORT_ALL_KEYS in Kitty keyboard
2026-04-01 08:44:32 -07:00
Anthony Kim 4a35a4880d Do not report lock key presses without REPORT_ALL_KEYS in Kitty keyboard protocol 2026-03-31 16:45:52 -07:00
Daniel Imms ad730f2d54 Merge pull request #5777 from Tyler-V/fix/fit-addon-same-dimension-guard
fix(addon-fit): remove redundant same-dimension guard in fit()
2026-03-26 08:09:53 -07:00
Daniel Imms 9c7c163dd4 Merge pull request #5776 from Gaubee/fix/headless-bun-deno-compat
Fix @xterm/headless compatibility with Bun and Deno runtimes
2026-03-26 08:05:03 -07:00
Daniel Imms 8674bc5e82 Merge pull request #5780 from PerBothner/extendedAttrs-test
Move extendedAttributes function to TestUtils
2026-03-26 07:58:38 -07:00
Daniel Imms d552be8c45 Merge pull request #5783 from xtermjs/dependabot/npm_and_yarn/multi-bf05dc1ecf
Bump picomatch
2026-03-26 07:49:45 -07:00
Daniel Imms 082a51127f Merge pull request #5779 from xtermjs/dependabot/npm_and_yarn/flatted-3.4.2
Bump flatted from 3.3.3 to 3.4.2
2026-03-26 07:49:32 -07:00
dependabot[bot] 89100f6a3d Bump picomatch
Bumps  and [picomatch](https://github.com/micromatch/picomatch). These dependencies needed to be updated together.

Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-25 21:44:04 +00:00
Per Bothner 8149d4481f Fix bad import. 2026-03-22 22:04:21 -07:00
Per Bothner 2d9b21f5c7 Move extendedAttributes function to TestUtils so it can be used by InputHandler.test.ts.
This is a followup to pull request #5748.
It removes the remaining test uses of internal _extendedAttrs.
2026-03-22 20:53:13 -07:00
dependabot[bot] f43985c422 Bump flatted from 3.3.3 to 3.4.2
Bumps [flatted](https://github.com/WebReflection/flatted) from 3.3.3 to 3.4.2.
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

---
updated-dependencies:
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-21 10:28:04 +00:00
Tyler VorpahlandClaude Opus 4.6 9f6c7d7ced fix(addon-fit): remove redundant same-dimension guard in fit()
Terminal.resize() already no-ops internally when dimensions haven't
changed. The addon's guard prevented resize() from being called at all,
forcing callers using ResizeObserver-based layouts to use perturbation
workarounds (resize to rows-1, then fit) that race with the observer.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 19:35:56 -04:00
GaubeeandClaude Opus 4.6 aa7716a333 Fix @xterm/headless compatibility with Bun and Deno runtimes
Replace `window` with `globalThis` in IdleTaskQueue export.
In Bun and Deno, `isNode` is false but `window` is undefined, causing
a ReferenceError. Using `globalThis` is safe in all JS runtimes and
makes the `isNode` guard unnecessary.

This is the only `in window` expression in code bundled into
@xterm/headless.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 10:30:12 +08:00
Daniel Imms 1432e20082 Merge pull request #5773 from xtermjs/dependabot/npm_and_yarn/yauzl-3.2.1
Bump yauzl from 2.10.0 to 3.2.1
2026-03-15 04:36:59 +09:00
Daniel Imms f1b0a96094 Merge pull request #5770 from mgabor3141/fix/viewport-sync-output-flicker
fix: defer viewport DOM sync during synchronized output (DEC 2026)
2026-03-15 04:35:17 +09:00
Daniel Imms 49096a21fd Delete src/browser/Viewport.test.ts
Trying to remove jsdom eventually, this test is also overly fragile, we'll just do without.
2026-03-15 04:27:01 +09:00
dependabot[bot] 63035678ec Bump yauzl from 2.10.0 to 3.2.1
Bumps [yauzl](https://github.com/thejoshwolfe/yauzl) from 2.10.0 to 3.2.1.
- [Commits](https://github.com/thejoshwolfe/yauzl/compare/2.10.0...3.2.1)

---
updated-dependencies:
- dependency-name: yauzl
  dependency-version: 3.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-13 20:11:05 +00:00
Daniel Imms 503abee8ae Merge pull request #5772 from Tyriar/5771
Prefer repeat over Array().join
2026-03-13 11:19:46 +09:00
Daniel Imms 4350536685 Prefer repeat over Array().join
Fixes #5771
2026-03-13 11:11:27 +09:00
mgabor3141 9feb963cc9 fix: defer viewport DOM sync during synchronized output
Defer Viewport DOM scroll updates while DEC 2026 synchronized output is
active, then flush once rendering resumes so DOM and canvas update
atomically.

Also update demo sizing to avoid relying on removed internal viewport
scrollBarWidth.
2026-03-12 18:51:38 +01:00
mgabor3141 f4042876c6 test: add viewport synchronized-output regression coverage (fails until fix) 2026-03-12 18:50:41 +01:00
mgabor3141 c294695444 refactor: plumb core service into Viewport constructor 2026-03-12 18:50:31 +01:00
Daniel Imms fe0952bb83 Merge pull request #5769 from Tyriar/4633
Only update necessary DOM range on selection change
2026-03-12 11:44:45 +09:00
Daniel Imms 5c2b19f2c8 Only update necessary DOM range on selection change
Fixes #4633
2026-03-12 11:36:33 +09:00
Daniel Imms 5e9b2cbc14 Update ImageRenderer.ts 2026-03-12 09:47:51 +09:00
sergiochan 3a59c8ec2b Fix image layer resize jitter
Disable desynchronized 2D canvas mode for image layers so resize paint stays synchronized and avoids blank-frame jitter on some platforms.

Signed-off-by: sergiochan <yuheng9211@qq.com>
2026-03-11 00:36:17 -07:00
Daniel Imms fb25eb8f79 Merge pull request #5764 from Tyriar/5675
Apply font styles directly to spans
2026-03-11 16:09:13 +09:00
Daniel Imms e7a4578933 Don't apply color to the spans 2026-03-11 16:01:41 +09:00
Daniel Imms a05a3321d5 Apply font styles directly to spans
The universal selector was overriding this since the font styles
only applied to the div, not the span.

Fixes #5675
2026-03-11 13:44:22 +09:00
Daniel Imms 26ce9bd4d3 Merge pull request #5698 from ntomoya/fix/ime-middle-composition-suffix
Fix IME input when composing in the middle of the textarea
2026-03-11 13:20:16 +09:00
Daniel Imms 1c1aac0da8 Update src/browser/input/CompositionHelper.ts 2026-03-11 13:12:36 +09:00
Daniel Imms beeebca1a4 Merge branch 'master' into fix/ime-middle-composition-suffix 2026-03-11 13:06:35 +09:00
Daniel Imms fd1e53c63f Merge pull request #5762 from Tyriar/5760
Fix RTL breaking IME composition rendering
2026-03-11 12:49:50 +09:00
Daniel Imms 18262c87d9 Merge pull request #5761 from SergioChan/fix/textarea-sync-on-resize-3390
fix: sync helper textarea position after resize
2026-03-11 12:48:14 +09:00
Daniel Imms 2285ac6ab8 Fix RTL breaking IME composition rendering
Fixes #5760
2026-03-11 12:41:48 +09:00
sergiochan 5302e62c90 fix: sync helper textarea position after terminal resize 2026-03-10 20:38:45 -07:00
Daniel Imms af6731b533 Merge pull request #5685 from Tyriar/5377_scroll
Fix scrolling with touch
2026-03-11 12:27:34 +09:00
Daniel Imms 5b9b531012 Merge branch 'master' into 5377_scroll 2026-03-11 12:19:42 +09:00
Daniel Imms b0118dfd3a Merge pull request #5759 from SergioChan/fix/ime-sync-on-compositionstart-5734
fix(ime): resync textarea position when composition starts
2026-03-11 12:17:26 +09:00
Daniel Imms 6813fc175e Merge remote-tracking branch 'upstream/master' into 5377_scroll 2026-03-11 12:15:59 +09:00
Daniel Imms 409f049f39 Update src/browser/CoreBrowserTerminal.ts 2026-03-11 12:02:02 +09:00
sergiochan 9e1b89651a fix(ime): resync textarea position on composition start 2026-03-10 19:36:17 -07:00
Daniel Imms 89980fc565 Merge pull request #5758 from Tyriar/5754__5756
CoreMouseService -> MouseStateService, make mouse services more focused
2026-03-11 11:10:52 +09:00
Daniel Imms 16b5da5c4c Fix assertion, bring back triggerMouseEvent tests 2026-03-11 11:03:17 +09:00
Daniel Imms 4812abb24f Merge branch 'master' into 5754__5756 2026-03-11 10:52:13 +09:00
Daniel Imms 5f3a4646be Move non-state methods into MouseService
Fixes #5756
2026-03-11 10:49:55 +09:00
Daniel Imms d63445be94 Move custom mouse event handler into MouseStateService
Don't need to keep it in CoreBrowserTerminal
2026-03-11 10:36:35 +09:00
Daniel Imms 1400398abb Merge pull request #5757 from Tyriar/5754
Move bindMouse into MouseService
2026-03-11 10:32:42 +09:00
Daniel Imms 2331abe6bd Rename CoreMouseService to MouseStateService
Part of #5756
2026-03-11 10:31:57 +09:00
Daniel Imms b66128b239 Avoid unnecessary 2026-03-11 10:24:46 +09:00
Daniel Imms c727240f16 Fix cyclic dependency
Linkifier -> MouseService -> SelectionService -> Linkifier
2026-03-11 10:23:38 +09:00
Daniel Imms 649ce22081 Move bindMouse into MouseService
Fixes #5754
2026-03-11 10:10:13 +09:00
Daniel Imms 73adfdfd56 Merge pull request #5753 from Tyriar/scrollbar_warn
Suppress scrollbar warning in demo
2026-03-11 09:44:38 +09:00
Daniel Imms 1fc8a4687d Suppress scrollbar warning in demo 2026-03-11 09:36:05 +09:00
Daniel Imms c29e48c917 Merge pull request #5751 from Tyriar/5750
Fix error in loadTestLongLines
2026-03-11 09:05:23 +09:00
Daniel Imms 70fe22993b Merge pull request #5752 from anthonykim1/anthonykim1/fixKittyDisambiguateSpaceEncoding
Fix kitty keyboard disambiguate mode for space, enter, tab, backspace
2026-03-11 08:59:12 +09:00
Daniel Imms 4f3a7331bd Fix error in loadTestLongLines
Fixes #5750
2026-03-11 08:57:51 +09:00
Anthony Kim 6f3ce7b0fe comment 2026-03-10 00:30:29 -07:00
Anthony Kim fdb7346f3a Fix kitty keyboard disambiguate mode for space, enter, tab, backspace 2026-03-10 00:27:27 -07:00
Daniel Imms 783c4a34d2 Merge pull request #5747 from anthonykim1/anthonykim1/fixIMEoverFlowing
Fix IME composition text overflowing terminal
2026-03-10 11:18:48 +09:00
Daniel Imms 8a1760b924 Merge pull request #5748 from PerBothner/extendedAttrs-test
Fix testsuite access to internal _extendedAttrs
2026-03-10 11:16:00 +09:00
Daniel Imms 211db9d067 Merge pull request #5749 from Tyriar/readme
Build out getting started section
2026-03-10 11:04:50 +09:00
Daniel Imms ec6ad20e33 Clean up note about cjs 2026-03-10 11:04:09 +09:00
Daniel Imms 47235f47fe Build out getting started section
Fixes #5725
2026-03-10 11:01:07 +09:00
Per Bothner 8d6ff3a27b Fix testsuite access to internal _extendedAttrs 2026-03-09 18:19:04 -07:00
Anthony Kim 55281022a0 Fix IME composition text overflowing terminal 2026-03-09 14:32:38 -07:00
Daniel Imms ea50fbdd1e Merge pull request #5742 from Tyriar/5721
work around Safari sending keycode 13 (enter) when typing ctrl-c (take 2)
2026-03-09 13:54:14 +09:00
Daniel Imms 87bf81cf96 Call out hack, fix lint 2026-03-09 13:44:02 +09:00
Serge f155c65e5a work around Safari sending keycode 13 (enter) when typing ctrl-c on iphone, iPad, AppleVisionPro 2026-03-09 13:42:36 +09:00
Daniel Imms 413c87cbf9 Merge pull request #5700 from xtermjs/dependabot/npm_and_yarn/qs-6.14.2
Bump qs from 6.14.1 to 6.14.2
2026-03-09 13:16:29 +09:00
Daniel Imms 1620f8f5a8 Merge pull request #5726 from Amtrend/master
docs: add SSH Connection Manager to real-world uses
2026-03-09 13:15:32 +09:00
Daniel Imms e769f80a14 Merge branch 'master' into master 2026-03-09 13:15:13 +09:00
Daniel Imms 6651c4ca23 Merge pull request #5741 from Tyriar/5740
Improve readability of celldata/chardata assertions in unit tests
2026-03-09 13:14:22 +09:00
Daniel Imms cb78a53f57 Merge pull request #5736 from ScottMorse/master
README Files: Fix deprecated package references for xterm-headless and xterm
2026-03-09 13:09:53 +09:00
Daniel Imms 1c7b9f6d79 Merge pull request #5729 from masterkain/patch-1
Add WooTTY project to README
2026-03-09 13:09:18 +09:00
Daniel Imms 0d535100e4 Merge pull request #5727 from xtermjs/dependabot/npm_and_yarn/minimatch-3.1.5
Bump minimatch from 3.1.2 to 3.1.5
2026-03-09 13:08:55 +09:00
Daniel Imms 5e19624e71 Merge pull request #5739 from xtermjs/anthonykim1/addressPerfxterm
Remove redundant bitmask in RGB->RGBA conversion
2026-03-09 13:08:27 +09:00
Daniel Imms 671425bf5a Use helpers in other tests 2026-03-09 13:03:00 +09:00
Daniel Imms df5393ec45 Use helper function and const for cell data tests 2026-03-09 12:54:27 +09:00
Daniel Imms b2553c17b4 Clarify indexes in CharData and IColorRGB
Part of #5740
2026-03-09 12:42:55 +09:00
Anthony Kim 0cea95d3b3 Make code more readable 2026-03-08 20:33:01 -07:00
Anthony Kim 163868ff60 Remove redundant bitmask in RGB->RGBA conversion 2026-03-07 23:32:02 -08:00
Anthony Kim f4166a87ba Merge pull request #5737 from xtermjs/anthonykim1/fixKittyKeyboardFish
Fix ^C not working to end for newer fish + kitty keyboard protocol
2026-03-06 09:47:52 -08:00
ScottMorse f7a353cd34 Fix npm link to deprecated xterm package to @xterm/xterm 2026-03-06 00:14:20 -07:00
ScottMorse 7ec41471da Fix deprecated references of xterm-headless to @xterm/headless 2026-03-06 00:10:32 -07:00
Anthony Kim f5e81afba7 Fix ^C not working to end for newer fish + kitty keyboard protocol 2026-03-05 16:46:03 -08:00
Anthony Kim a12061c899 Merge pull request #5733 from xtermjs/anthonykim/exposeImageCount
add onImageAdded to api
2026-03-03 13:11:03 -08:00
Anthony Kim d6c872bb2c add onImageAdded to api 2026-03-03 12:19:14 -08:00
Kain a2cb36e6dd Add WooTTY project to README
Added WooTTY to the list of projects using xterm.js.
2026-03-02 04:23:54 +01:00
dependabot[bot] 114c66c86c Bump minimatch from 3.1.2 to 3.1.5
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-28 03:15:03 +00:00
amtrend fe9650ce16 docs: add SSH Connection Manager to real-world uses 2026-02-22 17:32:48 +03:00
Anthony Kim dedf3b1595 Merge pull request #5722 from xtermjs/anthonykim1/placementActions
Implment kitty graphics placement action
2026-02-19 20:22:18 -08:00
Anthony Kim 2158827b25 remove non-null assertion, improve lifecycle for bitmap 2026-02-19 15:51:22 -08:00
Anthony Kim b406797dc7 Merge branch 'master' into anthonykim1/placementActions 2026-02-19 15:07:58 -08:00
Anthony Kim 5c28e27062 Implment kitty graphics placement action 2026-02-19 15:00:33 -08:00
Anthony Kim 6e35b44f02 Merge pull request #5719 from xtermjs/anthonykim1/rectangleCrop
Implement rectangle crop + sub-cell offset
2026-02-18 19:37:49 -08:00
Anthony Kim 30c97a225c Handle more edge cases 2026-02-18 14:41:58 -08:00
Anthony Kim 1186d09e05 add kitty graphics source crop (x/y/w/h) and sub-cell offset (X/Y) 2026-02-18 14:36:02 -08:00
Anthony Kim d99e6bb2bc Merge pull request #5718 from xtermjs/anthonykim1/fixZindex
Fix image addon overriding screen element z-index
2026-02-17 15:08:14 -08:00
Anthony Kim 579a0a530f Fix image addon overriding screen element z-index 2026-02-17 14:47:43 -08:00
Daniel Imms 0b6220e5ea Merge pull request #5716 from codekrafter/sanitize-bracketed-paste
feat: sanitize user text when in bracketed paste mode
2026-02-17 10:12:59 -08:00
Anthony Kim 0d119b30d6 Merge pull request #5717 from xtermjs/anthonykim1/constructorFixScreenDpr
Revert useDefineForClassFields, fix screenDpr Constructor
2026-02-17 09:38:29 -08:00
Anthony Kim 79c94de968 try not to use ! 2026-02-17 09:25:44 -08:00
Anthony Kim e3980a27db Revert useDefineForClassFields, fix screenDpr Constructor 2026-02-17 09:24:01 -08:00
Eli Edds 898c0d91c3 feat: sanitize user text when in bracketed paste mode 2026-02-17 12:22:25 -05:00
Daniel Imms 83401e3769 Merge pull request #5715 from xtermjs/anthonykim1/screendpr
Dont useDefineForClassField after tsgo migration
2026-02-17 08:48:22 -08:00
Anthony Kim 6b756318f0 Dont useDefineForClassField after tsgo migration 2026-02-17 08:31:53 -08:00
Daniel Imms 3a9bfa94bc Merge pull request #5619 from xtermjs/anthonykim1/scaffoldKittyAddon
Support Kitty graphics protocol mvp
2026-02-16 10:53:49 -08:00
Daniel Imms 209e5aa6b9 Revert formatting to demo/client/tsconfig.json 2026-02-16 07:56:50 -08:00
Anthony Kim 24e2eb9d7b Use uint32 bit-shifting for rgb->rgba interleaving 2026-02-15 22:15:43 -08:00
Anthony Kim d5ae437dda fix more mem leak + cleanups 2026-02-15 20:00:16 -08:00
Anthony Kim 5efff43e93 Convert JSDoc to line comments 2026-02-15 19:21:27 -08:00
Anthony Kim ee85a1da9e Copyright is to 2026 2026-02-15 19:15:58 -08:00
Anthony Kim c30fedff6b Start clean-up 2026-02-15 18:55:21 -08:00
Anthony Kim 21f51564d1 Fix image layer stacking + tile survival across text write 2026-02-14 20:55:55 -08:00
Anthony Kim 27b01b22ef Extract KittyImageStorage, prevent mem leak on eviction 2026-02-14 15:17:29 -08:00
Anthony Kim 387ec93a90 Merge remote-tracking branch 'upstream/master' into anthonykim1/scaffoldKittyAddon 2026-02-14 13:29:25 -08:00
Daniel Imms 4388a4ac13 Merge pull request #5695 from xtermjs/anthonykim1/realSeparateImageStoragePerMech
Separate image storage per mechanism (sixel, iip)
2026-02-13 12:42:11 -08:00
Daniel Imms 08c97d12b0 Merge pull request #5702 from Tyriar/hooks2
Fix hook npm script
2026-02-13 08:34:55 -08:00
Daniel Imms fbf1f7bc5d Fix hook npm script 2026-02-13 08:25:59 -08:00
Daniel Imms c8704930e3 Merge pull request #5701 from Tyriar/hooks
Add hook to setup repo when agent starts
2026-02-13 08:18:44 -08:00
Daniel Imms e60c96823d Add hook to setup repo when agent starts
Fixes #5605
2026-02-13 08:10:16 -08:00
dependabot[bot] 375be97615 Bump qs from 6.14.1 to 6.14.2
Bumps [qs](https://github.com/ljharb/qs) from 6.14.1 to 6.14.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.1...v6.14.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-13 15:15:15 +00:00
Daniel Imms 74642ce5d5 Merge pull request #5699 from Tyriar/tsgo
Migrate to tsgo
2026-02-13 07:14:27 -08:00
Daniel Imms eccbc60aa0 Migrate to tsgo
Fixes #5689
2026-02-13 06:59:24 -08:00
Anthony Kim 6b64660934 fix chunked transfer without repeated i = key 2026-02-12 20:04:21 -08:00
Anthony Kim 10182cc656 Reject unsupporteed transmission medium for a=t and a=T 2026-02-12 18:03:19 -08:00
Anthony Kim 3c7f313c34 Dont send false query 2026-02-12 16:00:48 -08:00
Anthony Kim 457542b7b3 Support deletion 2026-02-12 14:53:37 -08:00
Anthony Kim 36118d9d4c Merge upstream/master - resolve IIPHandler.ts const enum conflict 2026-02-12 13:47:48 -08:00
Anthony Kim cf8a4e466d Merge origin/master into anthonykim1/scaffoldKittyAddon 2026-02-12 13:44:21 -08:00
Anthony Kim 4de03aaa38 Try to reduce duplication from 1d0cf32 2026-02-12 11:03:46 -08:00
Anthony Kim 677e5275f9 Store z-index for drawing order 2026-02-12 10:27:32 -08:00
Tomoya Nakanishi c66b0ed168 Fix IME input when composing in the middle of the textarea
Track the composition range using the textarea selection and avoid re-sending trailing text that existed before composition started (notably in screenReaderMode).

Fixes #5456
2026-02-13 02:43:06 +09:00
Anthony Kim d4c3b693bb Remove unnecessary kittyAddon from test_integrations.js 2026-02-12 09:28:51 -08:00
Daniel Imms 2f66b5fdb6 Merge pull request #5697 from Tyriar/5696
Avoid forced layout in overview ruler impl
2026-02-12 07:09:35 -08:00
Daniel Imms 008aa1bfb5 Avoid forced layout in overview ruler impl
Fixes #5696
2026-02-12 07:00:56 -08:00
Anthony Kim 1d0cf32b28 support z-index + dual canvas for DOM. Doesnt work for webgl 2026-02-11 14:19:27 -08:00
Anthony Kim c01e4b7be7 Separate image storage per mechanism (sixel, iip) 2026-02-11 10:56:30 -08:00
Daniel Imms fafb195f0f Merge pull request #5693 from xtermjs/anthonykim1/demoPixelDimensions
Pass pixel dimensions to node-pty in demo
2026-02-11 10:47:14 -08:00
Anthony Kim 2c4997f529 Add test for 200x100 pixel 2026-02-11 10:27:12 -08:00
Anthony Kim f0d291f645 Merge pull request #5694 from xtermjs/anthonykim1/cleanDemoClientTypes
Correct alphabetical ordering in demo/client/types.ts
2026-02-11 09:45:22 -08:00
Daniel Imms f6b98291cf Merge pull request #5692 from xtermjs/anthonykim1/bumpWasmIIPHandlerChanges
Update wasm decoder to 0.3.0 changes in IIPHandler.ts
2026-02-11 05:43:44 -08:00
Anthony Kim 16ea2ec0ef Add image demo test for kitty 2026-02-10 21:32:41 -08:00
Anthony Kim f7118b19c6 Correct alphabetical ordering in demo/client/types.ts 2026-02-10 19:08:12 -08:00
Anthony Kim a0574f4e9c Pass pixel dimensions to node-pty in demo 2026-02-10 16:52:15 -08:00
Anthony Kim d9b5d150ae Put the const integer into local const enum, save indirection time at runtime 2026-02-10 16:31:17 -08:00
Anthony Kim a25db1c76d Update wasm to 0.3.0 changes in IIPHandler.ts 2026-02-10 14:35:34 -08:00
Daniel Imms c6a572fe21 Merge pull request #5688 from Tyriar/5687
Fix kitty modifier events when only flag 2 is used
2026-02-10 01:31:15 -08:00
Daniel Imms b9fa53169f Fix kitty modifier events when only flag 2 is used
Fixes #5687
2026-02-10 01:17:44 -08:00
Anthony Kim eb0f5383c3 question.. 2026-02-09 16:43:55 -08:00
Anthony Kim a4502a6d00 Impelement cursor movement after image placement 2026-02-09 14:30:35 -08:00
Anthony Kim c11a8f43f4 abort in-flight kitty chunks on delete 2026-02-09 13:34:27 -08:00
Anthony Kim d7ba56cd28 default format png -> rgba perspec 2026-02-09 13:19:41 -08:00
Anthony Kim be44833f12 send OK response for a=t, a=T transmissions when image id present 2026-02-09 13:04:34 -08:00
Anthony Kim fcb3cc1a21 add delelteImage, map kitty ID to storage IDs 2026-02-09 11:31:20 -08:00
Anthony Kim 376268157c add f=24 rgb, f=32 rgba format tests 2026-02-09 09:35:00 -08:00
Anthony Kim 5ceb02b9c6 Release decoder after command ahandler consumes bytes 2026-02-08 00:16:01 -08:00
Anthony Kim d320898231 offload raw image data to Blob for off-heap storage 2026-02-07 23:30:06 -08:00
Anthony Kim cf3939af4a Remove redundant _decodedImages bitmap cache 2026-02-07 23:11:19 -08:00
Daniel Imms 38913e4ae1 Fix scrolling with touch
Part of #5377
2026-02-07 16:09:03 -08:00
Daniel Imms e14746e6e9 Merge pull request #5682 from Tyriar/3440
Add attributesEqual and underline diff APIs
2026-02-07 06:37:19 -08:00
Daniel Imms 82333b9414 Remove unnecessary cast 2026-02-07 06:29:33 -08:00
Daniel Imms 9eeb2799c1 Remove internal details about underline cell APIs 2026-02-07 06:24:47 -08:00
Daniel Imms 5630471480 Merge pull request #5684 from Tyriar/5681
Make 0x1FB98 and 0x1FB99 perfectly tile
2026-02-07 06:24:05 -08:00
Daniel Imms e0c59e68b2 Make 0x1FB98 and 0x1FB99 perfectly tile
Fixes #5681
2026-02-07 06:12:47 -08:00
Daniel Imms cc8e680790 Add attributesEqual and underline diff APIs
Fixes #3440
2026-02-06 16:21:25 -08:00
Daniel Imms 452ee50c81 Merge pull request #5669 from Tyriar/custom_glyph_alignment
Use offset variants for some custom glyphs
2026-02-06 12:58:20 -08:00
Daniel Imms f41e53a0f0 Use correct non-inverse shade char in testWindow 2026-02-06 12:50:28 -08:00
Anthony Kim 596ab845d0 Make copy of bytes with Uint8Array since they can be reused by decoder 2026-02-05 22:04:04 -08:00
Anthony Kim a59d527d1a Mirror const enum since esbuild cant inline const enum 2026-02-05 21:38:33 -08:00
Daniel Imms b287397c4f Merge pull request #5680 from Tyriar/5678
Move overviewRuler API under scrollbar
2026-02-05 09:29:16 -08:00
Daniel Imms aebbe81252 Add overviewRuler options to demo 2026-02-05 09:19:26 -08:00
Daniel Imms d155bb6767 Move overviewRuler API under scrollbar
Fixes #5678
2026-02-05 09:14:58 -08:00
Daniel Imms 3a147e8e10 Merge pull request #5679 from Tyriar/scrollbar
Add scrollbar.showScrollbar
2026-02-05 07:28:45 -08:00
Daniel Imms c2db7d4c8a Add scrollbar.showScrollbar
Fixes #5676
2026-02-05 07:20:23 -08:00
Daniel Imms 456bc73bae Merge pull request #5677 from Tyriar/log_task
Use ILogService in TaskQueue
2026-02-05 06:49:05 -08:00
Daniel Imms 2e776c0820 Use ILogService in TaskQueue 2026-02-05 06:40:50 -08:00
Anthony Kim 08f85cf826 Use wasm-parts v0.3.0., remove manual sharding/leftover, update iip! 2026-02-05 00:20:39 -08:00
Daniel Imms 6f163beb97 Merge pull request #5674 from xtermjs/anthonykim1/addScreenElementAPI
Add api to expose screenElement
2026-02-04 04:40:37 -08:00
Daniel Imms b0d0bf8187 Update typings/xterm.d.ts 2026-02-04 04:32:37 -08:00
Anthony Kim 32e77c3b81 Lint 2026-02-03 23:24:57 -08:00
Anthony Kim e95ec8d4bc feed to decoder directly, try to clean things before getting upgraded base64 decoder 2026-02-03 17:26:59 -08:00
Anthony Kim cba6e3d191 Add api to expose screenElement 2026-02-03 15:49:07 -08:00
Anthony Kim d15b426bd4 Readme edit for kittySupport 2026-02-03 13:53:47 -08:00
Anthony Kim c907edd75f Separate out test, add failing//skip test for cursor placements 2026-02-03 13:53:33 -08:00
Daniel Imms 5741e450e6 Merge pull request #5665 from Tyriar/de_vs
Remove vs/ module
2026-02-03 07:14:08 -08:00
Daniel Imms 6ef653e589 Merge remote-tracking branch 'upstream/master' into de_vs 2026-02-03 07:05:50 -08:00
Daniel Imms a4c0e4690e Make arrows a little less fat 2026-02-03 07:00:33 -08:00
Daniel Imms a31f78bc8b Prefix all scrollbar classes with xterm- 2026-02-03 06:57:50 -08:00
Daniel Imms ca8e8546b5 Remove unused methods on scrollable element 2026-02-03 06:00:24 -08:00
Daniel Imms 3d473b52b6 Further merge and reduce
- Remove unused keyboardEvent module
- Trim unused browserEvent in OnStopCallback
- Inline single-use helpers to reduce module surface
- Remove createFastDomNode wrapper
2026-02-03 05:21:03 -08:00
Daniel Imms a2bec2aeab Merge pull request #5672 from Tyriar/blink_opt
Prevent blink interval from running unless needed
2026-02-03 05:15:45 -08:00
Daniel Imms 205cf6089d Prevent blink interval from running unless needed
Part of #944
2026-02-03 05:07:40 -08:00
Daniel Imms 8b0a6f9785 Enumerate all block pattern codepoints 2026-02-03 05:04:03 -08:00
Daniel Imms db0226be5c Add triangular shade chars fill tests 2026-02-03 04:59:10 -08:00
Daniel Imms ab5c06cf17 Merge abstract and smooth scrollable elements 2026-02-03 04:55:32 -08:00
Daniel Imms 9a778f0c98 Move back to CellColorResolver 2026-02-03 04:53:02 -08:00
Daniel Imms c102b4af50 Merge pull request #5671 from Tyriar/944
Implement blink rendering
2026-02-03 04:03:20 -08:00
Daniel Imms f3a7cac49f Handle options within blink state manager 2026-02-03 03:55:25 -08:00
Daniel Imms 0d2a58c9d0 Clean up lifecycle of blink manager 2026-02-03 03:48:28 -08:00
Daniel Imms 8a00898533 Implement blink rendering
Fixes #944
2026-02-03 03:46:21 -08:00
Daniel Imms edb0e1ceb3 Simplify, reduce
- Merge ScrollableElement into SmoothScrollableElement
- Prune unused helpers FastDomNode, Widget LinkedList
- Make internal exports non-exported
- Remove redundant guards/aliases
2026-02-03 03:42:19 -08:00
Daniel Imms 93c1650446 Move variant handling out of CellColorResolver.ts 2026-02-03 03:37:57 -08:00
Anthony Kim 1029e19b57 Pre-calculate + fix bug to make sure we dont reset per chunk 2026-02-02 21:27:47 -08:00
Anthony Kim d44900faa2 Parse control data early as suggested 2026-02-02 20:47:29 -08:00
Anthony Kim 5f724c5924 Reduce MAX_CONTROL_DATA_SIZE + EINVAL for i and I 2026-02-02 20:17:21 -08:00
Daniel Imms 77695cb405 Make arrows configurable via API 2026-02-02 12:33:33 -08:00
Daniel Imms dc4ebfd6e3 Use offset variants for some custom glyphs
Part of #5668
2026-02-02 08:07:04 -08:00
Daniel Imms fae6f03f90 Consolidate dom and browser helpers, remove unused 2026-02-02 07:09:33 -08:00
Daniel Imms d36af624cf Remove vs/, fix lint and build 2026-02-02 06:10:43 -08:00
Daniel Imms a105d3e457 Lint and remove more parts 2026-02-01 22:36:14 -08:00
Daniel Imms d5900c7eae Remove more parts 2026-02-01 22:22:29 -08:00
Daniel Imms beebe8b5dc Reduce size of scrollable impl 2026-02-01 21:28:18 -08:00
Anthony Kim ae58176bcb Lint 2026-02-01 13:22:02 -08:00
Anthony Kim 429b2882f1 Terminal eixst 2026-02-01 13:18:46 -08:00
Anthony Kim ffac8c03ab Merge branch 'master' into anthonykim1/scaffoldKittyAddon 2026-02-01 13:11:40 -08:00
Anthony Kim a7c4a160a8 flip inControlData thing like in C 2026-02-01 13:08:41 -08:00
Anthony Kim c9dc994090 TODO for file system stufff 2026-02-01 12:57:45 -08:00
Anthony Kim e4710d3286 Use sharding, decode with streaming 2026-02-01 12:37:09 -08:00
Daniel Imms 8f9045ed88 Merge pull request #5664 from Tyriar/renderer_tests_wsl
Get playwright tests reliably passing in wsl
2026-02-01 08:54:30 -08:00
Daniel Imms b8e5ec7304 Fix lint 2026-02-01 08:41:01 -08:00
Daniel Imms 6a3d2480e0 Get playwright tests reliably passing in wsl 2026-02-01 08:35:49 -08:00
Daniel Imms 735afde77f Merge pull request #5663 from Tyriar/image_demo
Move remaining addon test buttons into their own windows
2026-02-01 08:34:29 -08:00
Daniel Imms 53efc95170 Move remaining addon test buttons into their own windows 2026-02-01 08:26:20 -08:00
Daniel Imms 3512f140de Merge pull request #5658 from Tyriar/outline
Fix demo button outline
2026-02-01 08:23:59 -08:00
Daniel Imms dbe35428cb Merge pull request #5662 from Tyriar/any_fixes
Remove some easy any usages
2026-02-01 08:06:33 -08:00
Daniel Imms 297ac838a3 Fix build 2026-02-01 07:58:36 -08:00
Daniel Imms 5316cb0812 Remove some easy any usages 2026-02-01 07:48:40 -08:00
Daniel Imms c124550971 Merge pull request #5661 from Tyriar/iip
Fix copying array data in IIPHandler
2026-02-01 07:23:39 -08:00
Daniel Imms 6525a7e68e Fix copying array data in IIPHandler
Fixes #5660
2026-02-01 07:15:25 -08:00
Daniel Imms f666135881 Move all impl to scrollable/ 2026-02-01 06:25:12 -08:00
Daniel Imms b2133d93f0 Merge pull request #5659 from Tyriar/dead_code2
Remove unreachable code in SGR handling 100
2026-02-01 06:07:15 -08:00
Daniel Imms a5e4f90139 Remove unreachable code in SGR handling 100
The implementation seems to be wrong. This looks like a remnant left over from
term.js before bright SGR was implemented.
2026-02-01 05:59:00 -08:00
Daniel Imms a27b8feb8f Fix demo button outline 2026-01-31 22:56:48 -08:00
Daniel Imms eea03cf0a9 Use lightweight versions of most vs/ imports 2026-01-31 22:52:19 -08:00
Daniel Imms e96748cc79 Merge pull request #5657 from Tyriar/inst2
Correct test copilot instructions
2026-01-31 22:34:43 -08:00
Daniel Imms 334c8170fe Merge pull request #5654 from Tyriar/5612
win32 input mode: Avoid scrolling to bottom on modifier only
2026-01-31 22:34:02 -08:00
Daniel Imms 2bb9662bea Merge pull request #5656 from Tyriar/cancel
Remove cancelEvent/cancel function
2026-01-31 22:26:50 -08:00
Daniel Imms 7cba3c9fa1 Correct test copilot instructions 2026-01-31 22:26:06 -08:00
Daniel Imms bab7db13db Remove cancelEvent/cancel function 2026-01-31 22:18:32 -08:00
Daniel Imms 1c79c5511c Merge pull request #5653 from Tyriar/input_handler_tests
Add remaining input handler tests, unskip modes that don't change
2026-01-31 22:09:48 -08:00
Daniel Imms 7b417f4585 win32 input mode: Avoid scrolling to bottom on modifier only
Fixes #5612
2026-01-31 22:05:33 -08:00
Daniel Imms dc487dfd6f Assert no mode changes and unskip 2026-01-31 22:01:35 -08:00
Daniel Imms 36baf70bea Unskip supported InputHandler tests 2026-01-31 21:05:36 -08:00
Daniel Imms 87e0b9ab10 Merge pull request #5652 from Tyriar/lint_test
Enable linting on test/ folder
2026-01-31 21:04:36 -08:00
Daniel Imms 5e532c13c6 Enable linting on test/ folder 2026-01-31 20:57:51 -08:00
Daniel Imms db131e0c22 Merge pull request #5651 from Tyriar/ih_tests
Implement more InputHandler tests
2026-01-31 18:28:42 -08:00
Daniel Imms 51b9a713da Implement more InputHandler tests
Part of #2117
2026-01-31 18:21:32 -08:00
Daniel Imms 972c069fa3 Merge pull request #5650 from Tyriar/slow_tests_3
Speed up TextDecoder.test.ts
2026-01-31 16:48:31 -08:00
Daniel Imms 2c0ba3c058 Speed up TextDecoder.test.ts
- Reduce calls to decode and clear
- Consolidate numberic checks
- Increase batch size
2026-01-31 16:42:11 -08:00
Daniel Imms e515647f99 Merge pull request #5649 from Tyriar/promise
Clear dangling timeout that keeps unit tests event loop alive
2026-01-31 16:27:25 -08:00
Daniel Imms 7a2794a552 Clear dangling timeout that keeps unit tests event loop alive
This fixes the issue where test-unit could run for up to 5 seconds after
the tests have already finished.
2026-01-31 16:21:03 -08:00
Daniel Imms da2267a10f Merge pull request #5648 from Tyriar/slow_tests_2
Speed up all remaining unit tests below threshold (40ms?)
2026-01-31 16:09:46 -08:00
Daniel Imms 34e68b1ad4 Speed up all remaining unit tests below threshold (40ms?) 2026-01-31 15:50:51 -08:00
Daniel Imms 1c42957461 Merge pull request #5646 from Tyriar/slow_tests
Speed up slow payload limit parser tests
2026-01-31 15:40:10 -08:00
Daniel Imms 77036d3f12 Merge pull request #5647 from Tyriar/fix_benchmark
Fix benchmark usage, add instructions
2026-01-31 15:37:17 -08:00
Daniel Imms ccbf612ddb Fix lint 2026-01-31 15:33:49 -08:00
Daniel Imms eed8423030 Clarify runtime cases 2026-01-31 15:30:16 -08:00
Daniel Imms 431d511c95 Fix benchmark usage, add instructions 2026-01-31 15:28:11 -08:00
Daniel Imms 0ca1bd48be Inline payload limit 2026-01-31 15:21:48 -08:00
Daniel Imms 7ceb15bb83 Reduce payload size to speed up unit tests
Reduces 11s -> 8s for full run
2026-01-31 15:17:10 -08:00
Daniel Imms 5ecdc7da44 Make payload limit configurable by tests 2026-01-31 15:08:05 -08:00
Daniel Imms 2e2270d01f Merge pull request #5645 from Tyriar/events
Optimize Emitter.fire for 0 and 1 listeners
2026-01-31 14:51:01 -08:00
Daniel Imms 042bb4bed9 Optimize Emitter.fire for 0 and 1 listeners
Before:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 97.77 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 49.30 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 35.37 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.32 MB/s

After:
      Context "Emitter.fire()"
         Context "0 listeners"
            Case "#1" : 1 runs - average throughput: 376.94 MB/s
         Context "1 listener"
            Case "#1" : 1 runs - average throughput: 84.29 MB/s
         Context "2 listeners"
            Case "#1" : 1 runs - average throughput: 32.36 MB/s
         Context "5 listeners"
            Case "#1" : 1 runs - average throughput: 15.93 MB/s
2026-01-31 14:42:23 -08:00
Daniel Imms 7e3c41ba49 Merge pull request #5643 from Tyriar/cacheWorkCell
Cache work cell in hot loops
2026-01-31 10:33:25 -08:00
Daniel Imms 21e4301e38 Merge pull request #5642 from Tyriar/startsWith
Prefer startsWith over indexOf === 0
2026-01-31 10:27:15 -08:00
Daniel Imms 3ef43aedb1 Cache work cell in hot loops 2026-01-31 10:26:15 -08:00
Daniel Imms 9eaf5b45dc Merge pull request #5641 from Tyriar/padStart
Use native padStart over custom impl
2026-01-31 10:25:24 -08:00
Daniel Imms 39037ee806 Prefer startsWith over indexOf === 0
Faster and make code clearer
2026-01-31 10:20:44 -08:00
Daniel Imms e542affaca Use native padStart over custom impl 2026-01-31 10:17:52 -08:00
Daniel Imms 038883c41b Merge pull request #5640 from Tyriar/strict_demo
Make demo use strict TS compiler mode
2026-01-31 09:32:36 -08:00
Daniel Imms 6c3d4e4ea4 Fix lint 2026-01-31 09:26:05 -08:00
Daniel Imms 659cfddd8c Merge branch 'master' into strict_demo 2026-01-31 09:24:47 -08:00
Daniel Imms c375696ee6 Make demo use strict TS compiler mode 2026-01-31 09:22:45 -08:00
Daniel Imms bc3c376e83 Merge pull request #5628 from Tyriar/5626
Support color scheme reporting
2026-01-31 09:18:33 -08:00
Daniel Imms 9fca3d8ecf Merge branch 'master' into 5626 2026-01-31 09:12:15 -08:00
Daniel Imms 8eed7b4163 Merge pull request #5639 from Tyriar/nullish
Add @typescript-eslint/prefer-nullish-coalescing eslint rule
2026-01-31 09:03:35 -08:00
Daniel Imms c29938ee83 Merge pull request #5634 from Tyriar/const_enum
Use const enum in EscapeSequences
2026-01-31 09:00:38 -08:00
Daniel Imms 0e679ea6ae Use nullish coalescing in KeyboardService 2026-01-31 08:56:53 -08:00
Daniel Imms d0e8141f7c Merge branch 'master' into nullish 2026-01-31 08:56:14 -08:00
Daniel Imms 1d322c9e45 Fix name to follow enum rules 2026-01-31 08:54:15 -08:00
Daniel Imms bad7e329df Add @typescript-eslint/prefer-nullish-coalescing eslint rule 2026-01-31 08:43:05 -08:00
Daniel Imms ae5e7e55c8 Merge pull request #5638 from Tyriar/tyriar/comma
Add @stylistic/comma-spacing eslint rule
2026-01-31 08:32:49 -08:00
Daniel Imms 5bb2e46508 Merge pull request #5630 from xtermjs/anthonykim1/updateNodePtyVersion
Update node-pty to ^1.2.0-beta.9
2026-01-31 08:31:09 -08:00
Daniel Imms 5938b8823d Add @stylistic/comma-spacing eslint rule 2026-01-31 07:38:16 -08:00
Daniel Imms 69d87c8a30 Merge pull request #5637 from Tyriar/remove_trace
Remove trace decorator and unused platform consts
2026-01-30 07:34:45 -08:00
Daniel Imms a247154650 Merge pull request #5636 from Tyriar/win32__kitty
Move kitty input mode into a class to avoid init cost
2026-01-30 07:29:42 -08:00
Daniel Imms b30d9c3533 Remove trace decorator and unused platform consts 2026-01-30 07:27:38 -08:00
Daniel Imms 60190c1c48 Merge branch 'master' into win32__kitty 2026-01-30 07:23:07 -08:00
Daniel Imms 99d69d2236 Merge pull request #5635 from Tyriar/win32
Move win32 input mode into a class to avoid init cost
2026-01-30 07:23:01 -08:00
Daniel Imms de01d4608c Move kitty input mode into a class to avoid init cost 2026-01-30 07:09:48 -08:00
Daniel Imms d35481c3f9 Move win32 input mode into a class to avoid init cost 2026-01-30 07:07:55 -08:00
Daniel Imms e2d84928b7 Merge pull request #5633 from Tyriar/dead_code
Remove unused TypedArrayUtils.ts
2026-01-30 07:00:49 -08:00
Daniel Imms 9ffd16aa9b Use const enum in EscapeSequences
They'll be inlined and only the used ones will show in bundle
2026-01-30 06:55:50 -08:00
Daniel Imms 77fa03d11c Remove unused TypedArrayUtils.ts 2026-01-30 06:53:02 -08:00
Daniel Imms 01450f044a Merge pull request #5632 from Tyriar/5631
Remove vs dependency in common
2026-01-30 06:39:28 -08:00
Daniel Imms b04d816d8a Undo changes to headless/package.json 2026-01-30 06:32:49 -08:00
Daniel Imms f80dbf3d03 Fix lint, split up namespace and interface 2026-01-30 06:21:15 -08:00
Daniel Imms b58bd452af Fix lint 2026-01-30 06:07:30 -08:00
Daniel Imms e8d99d50a6 Remove vs dependency in common
Fixes #5631
2026-01-30 06:03:16 -08:00
Anthony Kim bae314f67c Try using the wasm base64 decoder 2026-01-29 01:12:09 -08:00
Anthony Kim cb8159c4f2 Start integrating KittyGraphics stuff into imageAddon as suggested 2026-01-29 00:53:10 -08:00
Anthony Kim b87cba4c2a Update node-pty to ^1.2.0-beta.9 2026-01-28 14:04:46 -08:00
Anthony Kim 492d950b64 Add TODOs from jerch's comments 2026-01-28 09:17:44 -08:00
Anthony Kim a1b69e5ac8 TODO on wasm stuff 2026-01-27 23:26:38 -08:00
Anthony Kim d77711936f comments on file system 2026-01-27 23:22:31 -08:00
Anthony Kim 5a79b08661 stale comments 2026-01-27 23:12:13 -08:00
Anthony Kim 3e8fc48b33 Small details 2026-01-27 22:20:21 -08:00
Anthony Kim 8c9afdb364 Outsource a bunch to KittyApcHandler. TODO: go over this 2026-01-27 22:13:47 -08:00
Anthony Kim f89524e577 Create KittyApchandler.ts 2026-01-27 21:25:31 -08:00
Anthony Kim a1c1702508 Assemble and use dataArray for ImageData, make hot-loop faster 2026-01-27 20:25:10 -08:00
Anthony Kim 15c71e28c8 Replace more magic numbers with readable symbols 2026-01-27 19:51:16 -08:00
Anthony Kim fd641370ec Dont use magic number, use symbol 2026-01-27 19:22:59 -08:00
Anthony Kim 259cc67f12 Handle actions, compression first, then parseInt before Switch 2026-01-27 19:20:11 -08:00
Anthony Kim b0b0868e90 Conste num for keys, describe in jsdoc 2026-01-27 19:14:06 -08:00
Anthony Kim d58e8597cd Use Terminal.dimensions intead 2026-01-27 19:04:17 -08:00
Anthony Kim 17d439515f switch from triggerDataEvnet to this._terminal.input 2026-01-27 18:59:48 -08:00
Anthony Kim ae171acb79 Dont forget about action o for compression 2026-01-27 18:44:40 -08:00
Anthony Kim e2dabc77fd Prefer ?? and move disposable location 2026-01-27 17:58:10 -08:00
Anthony Kim 40342ecd1e remove send-png 2026-01-27 17:26:06 -08:00
Anthony Kim 1442f84e4e Fix chunking for large data, allow pixels 2026-01-27 17:21:38 -08:00
Daniel Imms 6f87ae77a7 Support color scheme reporting
CSI ? 996 n
CSI ? 2031 h
CSI ? 2031 l

Fixes #5626
2026-01-27 10:19:26 -08:00
Daniel Imms 5f25e05cd2 Merge pull request #5625 from Tyriar/kitty_shift_disambiguate
Don't disambiguous kitty keyboard on shift only
2026-01-26 14:51:17 -08:00
Daniel Imms 2af9d10171 Don't disambiguous kitty keyboard on shift only
Part of microsoft/vscode#286809
2026-01-26 14:44:34 -08:00
Daniel Imms f471c0add8 Merge pull request #5624 from xtermjs/Tyriar-patch-1
Don't set windows input mode on demo
2026-01-26 13:19:09 -08:00
Daniel Imms 62dc0d9c51 Don't set windows input mode on demo 2026-01-26 13:12:37 -08:00
Anthony Kim 5e95ef652d more comments 2026-01-23 11:11:02 -08:00
Daniel Imms 14525b334f Merge pull request #5618 from xtermjs/anthonykim1/lastState1
Add STATE_LENGTH to equal last state + 1
2026-01-23 04:10:06 -08:00
Anthony Kim bc1d87dade comments 2026-01-22 23:38:10 -08:00
Anthony Kim 849110d300 add query to say OK responses 2026-01-22 18:40:37 -08:00
Anthony Kim 8b6f8f3172 Rename 2026-01-22 16:16:30 -08:00
Anthony Kim 27825f9ed8 param in kittyGraphicsAddon.ts , ci.yml 2026-01-22 15:59:46 -08:00
Anthony Kim a507afb388 Add rendering, tweak send-png 2026-01-22 15:41:48 -08:00
Anthony Kim ec5b421dfb Renderer + og formatting for ci.yml 2026-01-22 15:40:59 -08:00
Anthony Kim 8f0e711a79 ci.yml 2026-01-22 13:09:44 -08:00
Anthony Kim fc0dc3fb07 Dont mess with og formatting for ci.yml 2026-01-22 13:06:42 -08:00
Anthony Kim bea1628015 Edit ci.yml ? 2026-01-22 12:02:46 -08:00
Anthony Kim 447746314e npm install to update pckage-lock.json 2026-01-22 11:58:51 -08:00
Anthony Kim 6a02d7c9f5 Add some playwright tests that will first fail 2026-01-22 11:56:48 -08:00
Anthony Kim 1ace108422 better comments 2026-01-22 10:54:41 -08:00
Anthony Kim 2c1441a52d Add STATE_LENGTH to equal last state + 1 2026-01-22 10:49:21 -08:00
Anthony Kim 304943658d Support Kitty graphics protocol 2026-01-22 10:44:38 -08:00
Daniel Imms 8e67c0093c Merge pull request #5617 from xtermjs/anthonykim1/kittyGraphicsProtocol
Add register apc handler
2026-01-22 04:35:38 -08:00
Anthony Kim ee3b3626e8 Again, format 2026-01-21 16:27:19 -08:00
Anthony Kim b286f21f64 Add APC_STRING since DCS_PASSTHROUGH isnt the last in ParserState anymore 2026-01-21 15:50:51 -08:00
Anthony Kim 3d92a7e2ed Dont be silly with original formatting 2026-01-21 15:13:03 -08:00
Anthony Kim b01ba7db28 Add apc tests parser.test.ts 2026-01-21 14:17:01 -08:00
Anthony Kim 1c73006e39 Switch TransitionTable to Uint16Array 2026-01-21 13:41:29 -08:00
Anthony Kim 03b015d5d3 Add registerApcHandler to xterm-headless.d.ts 2026-01-21 11:45:21 -08:00
Anthony Kim 42d408a9a7 Change ordering in escapeSequnceParser.ts 2026-01-21 11:29:04 -08:00
Anthony Kim 5ecd980a83 Comment 2026-01-21 11:01:09 -08:00
Anthony Kim 63ebe2df5b Stop messing with existing formatting 2026-01-21 11:00:10 -08:00
Anthony Kim 1b2d361b59 Remove comment on Constant.ts for ApcState 2026-01-21 10:59:33 -08:00
Anthony Kim 8e29591b9f Add registerApcHandler to public api 2026-01-21 10:42:36 -08:00
Anthony Kim 48df6117ed Add ApcParser 2026-01-21 08:31:07 -08:00
Daniel Imms 14b6a71391 Merge pull request #5615 from Tyriar/5585
Fix stack overflow in webgl page merge logic
2026-01-19 04:07:10 -08:00
Daniel Imms c24eb60bd3 Fix stack overflow in merge logic
Fixes #5585
2026-01-18 15:45:19 -08:00
Daniel Imms 2521bab72f Merge pull request #5613 from Tyriar/tyriar/5611
Fix some control keys not sending correct sequences
2026-01-16 06:14:01 -08:00
Daniel Imms 0f1c9a47db Fix some control keys not sending correct sequences
Fixes #5611
2026-01-16 06:06:55 -08:00
Daniel Imms 777a46445a Merge pull request #5609 from xtermjs/merogge/show-cursor
add `showCursorImmediately` option
2026-01-13 08:47:50 -08:00
meganrogge a20ff565b0 add showCursorImmediately option 2026-01-13 10:36:35 -06:00
Daniel Imms d28046ebed Merge pull request #5607 from Tyriar/5606
Fix blur due to canvas dim mismatch in glyph renderer
2026-01-13 06:35:40 -08:00
Daniel Imms 4ddd982e42 Fix blur due to canvas dim mismatch in glyph renderer
Fixes #5606
2026-01-13 06:23:10 -08:00
Daniel Imms a3228bd1b8 Merge pull request #5604 from Tyriar/2357_2
Fix capital letters and ctrl+letter in win32-input-mode
2026-01-11 09:10:31 -08:00
Daniel Imms a1fca4feb8 Fix ctrl+letter when inside pwsh->WSL 2026-01-11 09:02:44 -08:00
Daniel Imms afc6f0b7eb Fix caps not working in win32InputMode
Part of #2357
2026-01-11 09:00:08 -08:00
Daniel Imms f4c77d37b4 Merge pull request #5603 from Tyriar/2357
Implement win32-input-mode
2026-01-10 06:04:17 -08:00
Daniel Imms 9d0beb7c38 Use 0b over 0x for flags 2026-01-10 05:57:13 -08:00
Daniel Imms f88650ec1c Use win32InputMode in demo on Windows 2026-01-10 05:54:48 -08:00
Daniel Imms 8b35c67dd3 useWin32 -> useWin32InputMode 2026-01-10 05:52:19 -08:00
Daniel Imms 53dc85eca2 Add copyright 2026-01-10 05:48:48 -08:00
Daniel Imms 0946386cc4 Make unit tests more compact 2026-01-10 05:48:30 -08:00
Daniel Imms ebe18b3da5 Add more test cases 2026-01-10 05:42:17 -08:00
Daniel Imms 6c2792a1d5 Fix tests 2026-01-10 05:33:08 -08:00
Daniel Imms ee47054678 Implement win32-input-mode
Fixes #2357
2026-01-10 05:27:03 -08:00
Daniel Imms 91c4761b82 Merge pull request #5600 from Tyriar/4198_kitty
Implement kitty keyboard protocol (CSI =|?|>|< u)
2026-01-10 04:46:38 -08:00
Daniel Imms c9fc6fca8e Lint 2026-01-10 04:39:26 -08:00
Daniel Imms 73cba6dda3 Clean up, add sgr extension to api 2026-01-10 04:38:06 -08:00
Daniel Imms 50b0f3d7ee Add vtExtensions.kittySgrBoldFaintControl 2026-01-10 04:31:36 -08:00
Daniel Imms 686c67a14b Make mandatory browser services readonly 2026-01-10 04:20:29 -08:00
Daniel Imms ec78500f86 Create IKeyboardService 2026-01-10 04:19:23 -08:00
Daniel Imms 98ea14824a Make lint happy 2026-01-09 13:14:30 -08:00
Daniel Imms 4c905dad33 Update API jsdoc 2026-01-09 13:02:18 -08:00
Daniel Imms e3fdcc7869 Fix API lint 2026-01-09 13:01:22 -08:00
Daniel Imms 0674663a5a Remove unused import 2026-01-09 12:56:45 -08:00
Daniel Imms e07832a420 Merge branch 'master' into 4198_kitty 2026-01-09 12:51:48 -08:00
Daniel Imms 934b5dc8fa Merge pull request #5602 from Tyriar/5601
Add SGR 221 and 222
2026-01-09 12:21:43 -08:00
Daniel Imms 7a544d8346 Add SGR 221 and 222
Fixes #5601
2026-01-09 12:13:40 -08:00
Daniel Imms c624a47780 Maintain different buffer flags, stack limit, tests 2026-01-09 11:59:50 -08:00
Daniel Imms 5a29d489c3 Separate out kitty keyboard handlers 2026-01-09 11:46:57 -08:00
Daniel Imms 4a67836a5b Remove support responses
Wrong place for them
2026-01-09 11:42:00 -08:00
Daniel Imms 3adf6be988 Fix some edge cases 2026-01-09 10:36:13 -08:00
Daniel Imms 566589f7c1 More comprehensive tests 2026-01-09 07:23:12 -08:00
Daniel Imms 8dbc91dc24 Add vtExtensions to demo 2026-01-09 07:15:45 -08:00
Daniel Imms 992da3cc4d Hook up vt extensions option 2026-01-09 07:07:10 -08:00
Daniel Imms 3fbaef06ef Align behavior closer with kitty 2026-01-09 06:53:12 -08:00
Daniel Imms 2265fa2ade Implement kitty keyboard protocol (CSI u)
Fixes #4198
2026-01-09 05:59:57 -08:00
Daniel Imms 756c353237 Merge pull request #5593 from kallsyms/improve-lines-cache-perf
improve SearchLineCache performance
2026-01-09 05:59:54 -08:00
Daniel Imms 49a8ac8fc0 Merge pull request #5599 from Tyriar/flush
Flush writes on resize
2026-01-09 05:37:21 -08:00
Daniel Imms bf7c95b6cc Flush writes on resize
Fixes #5597
2026-01-09 05:25:22 -08:00
Daniel Imms 8112fbf210 Merge pull request #5598 from Tyriar/5588
Fix exception on resize during write
2026-01-08 18:10:41 -08:00
Daniel Imms 16a13f8504 Fix exception on resize during write
Fixes #5588
2026-01-08 18:03:42 -08:00
Daniel Imms 9bd128b315 Merge pull request #5594 from Tyriar/onSelectionChangeClear
Fire onSelectionChange on clear
2026-01-08 17:44:16 -08:00
Daniel Imms 729a670043 Merge pull request #5596 from Tyriar/lint_changes
New npm script to lint only changes
2026-01-08 17:39:34 -08:00
Daniel Imms bd8454db55 Another poll case 2026-01-08 17:37:44 -08:00
Daniel Imms d6f6a6af9a Lint instructions 2026-01-08 17:32:50 -08:00
Daniel Imms d65f9312ea Add lint-changes npm scripts 2026-01-08 17:32:15 -08:00
Daniel Imms a437d12004 Poll to avoid a race 2026-01-08 17:25:26 -08:00
Daniel Imms 437490308c Merge pull request #5595 from Tyriar/download_fonts
Remove download fonts
2026-01-08 17:18:36 -08:00
Daniel Imms 3250c31202 Remove download fonts 2026-01-08 17:09:26 -08:00
Daniel Imms 55d12e03d1 Remove unneeded test 2026-01-08 17:04:35 -08:00
Daniel Imms 6efb319ece Add tests more thorough tests for onSelectionChange 2026-01-08 17:01:56 -08:00
Daniel Imms b69afe5765 Merge remote-tracking branch 'upstream/master' into onSelectionChangeClear 2026-01-08 16:51:54 -08:00
Daniel Imms 613aa6ba23 Fire onSelectionChange when starting to drag 2026-01-08 16:51:45 -08:00
Nick Gregory a8c9128841 improve SearchLineCache performance 2026-01-07 16:04:49 -08:00
Daniel Imms 94a264679e Merge pull request #5591 from Tyriar/5586
Ensure mouse listeners are removed on dispose
2026-01-05 12:38:41 -08:00
Daniel Imms cb35d1b487 Ensure mouse listeners are removed on dispose
Also use ? over ! just in case

Fixes #5586
2026-01-05 12:31:54 -08:00
Daniel Imms c97abced76 Merge pull request #5590 from Tyriar/5587
Add undefined checks in erase calls
2026-01-05 11:17:40 -08:00
Daniel Imms 9db31db886 Add undefined checks in erase calls
This is likely a race condition during a resize which is hard to say
what the right behavior is. Instead of hard failing by throwing an
error, silently ignore if this happens.

Fixes #5587
2026-01-05 11:10:56 -08:00
Daniel Imms ccf9727b85 Merge pull request #5583 from Tyriar/root
Clean up root, remove auto approve settings
2026-01-04 06:44:57 -08:00
Daniel Imms 1dfdde9e27 Clean up root, remove auto approve settings
These are automatically auto approved in upcoming vscode
2026-01-04 06:38:53 -08:00
Daniel Imms 2712e88d0a Merge pull request #5582 from Tyriar/readme2
Update readme 2
2026-01-04 06:28:32 -08:00
Daniel Imms b5da8a28f6 Update readme 2 2026-01-04 06:27:22 -08:00
Daniel Imms 66c4f8b116 Merge pull request #5581 from Tyriar/readme
Update readme
2026-01-04 06:20:48 -08:00
Daniel Imms 9148c80d1a Update readme
- Added x2 scale logo
- Revised wording
- Added new addons
2026-01-04 06:18:50 -08:00
Daniel Imms e1bfc7dd1f Merge pull request #5178 from jerch/webfonts-addon
Add webfonts addon
2026-01-04 05:43:01 -08:00
Daniel Imms c6c4a2ffbc Delete unwanted eslintrc 2026-01-03 15:39:24 -08:00
Daniel Imms 2cf2929869 Publish webfonts 2026-01-03 15:36:36 -08:00
Daniel Imms b0145998c5 Fix lint 2026-01-03 15:36:31 -08:00
Daniel Imms 0e57643f16 Move fonts into folder 2026-01-03 15:33:13 -08:00
Daniel Imms a35223a819 Add web fonts window to demo 2026-01-03 15:31:24 -08:00
Daniel Imms 5e95dc1f10 Sync package lock 2026-01-03 15:24:11 -08:00
Daniel Imms b4e9a7f9ea Merge remote-tracking branch 'upstream/master' into pr/jerch/5178 2026-01-03 15:21:50 -08:00
Daniel Imms 73e3357526 Merge pull request #5579 from Tyriar/proposed
Make everything except unicode APIs stable
2026-01-03 15:04:25 -08:00
Daniel Imms 483c7b4a81 Merge pull request #5580 from Tyriar/5572
Update demo dims on font family change
2026-01-03 14:58:57 -08:00
Daniel Imms f4453b3e18 Merge pull request #5566 from xtermjs/dependabot/npm_and_yarn/qs-6.14.1
Bump qs from 6.14.0 to 6.14.1
2026-01-03 14:58:39 -08:00
Daniel Imms 7389a1097d Update tests to use unicode for proposed check 2026-01-03 14:57:55 -08:00
Daniel Imms 0c7b1cb448 Update demo dims on font family change
Fixes #5572
2026-01-03 14:52:32 -08:00
Daniel Imms 7a3b5339e7 Merge pull request #5578 from Tyriar/5576
Lint demo code
2026-01-03 14:41:48 -08:00
Daniel Imms 8193ceae12 Make everything except unicode APIs stable 2026-01-03 14:40:07 -08:00
Daniel Imms 064e299bb0 Merge pull request #5577 from Tyriar/ext_recommendation
Recommend eslint extension in vscode
2026-01-03 14:38:45 -08:00
Daniel Imms 6211bf078c Lint demo code
Fixes #5576
2026-01-03 14:35:39 -08:00
Daniel Imms a639808196 Recommend eslint extension in vscode
Fixes #5575
2026-01-03 14:32:16 -08:00
Daniel Imms b522e255f0 Merge pull request #5571 from Tyriar/5341
Fix isNode detection when process module is used in webpack
2026-01-03 14:25:33 -08:00
Daniel Imms d7850af9a2 Fix lint 2026-01-03 14:19:36 -08:00
Daniel Imms 4182f2226f Merge pull request #5573 from Tyriar/5570_oom
Fix infinite recursion OOM crash in addon-ligatures
2026-01-03 14:15:11 -08:00
Daniel Imms e2f45b5f4e Fix for modern node that includes a navigator.userAgent 2026-01-03 14:10:44 -08:00
Daniel Imms 8712f8067d Fix infinite recursion OOM crash in addon-ligatures
1. processLookahead/BacktrackPosition created separate entry objects
   for each glyph at each position (O(N^M) with N glyphs, M positions).
   Now all glyphs at a position share one entry object.
2. cloneEntry, cloneTree, flattenEntry, and mergeTreeEntry could
   infinitely recurse or duplicate work on shared/cyclic references.
   Added visited/merged tracking maps to cache results.

Note that parsing CommitMono locks up the renderer for some time still
so there is still a performance issue here, this fixes the OOM though.
This was mostly defensive generated code since the codebase is
unfamiliar to me.

Fixes #5570
2026-01-03 14:06:57 -08:00
Daniel Imms 08c49301b7 Fix isNode detection when process module is used in webpack
Fixes #5341
2026-01-03 11:19:23 -08:00
dependabot[bot] 917a8ded92 Bump qs from 6.14.0 to 6.14.1
Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-03 17:44:20 +00:00
Daniel Imms d06d278f0c Merge pull request #5569 from Tyriar/5568_font_ligatures
Merge font-ligatures package into addon-ligatures
2026-01-03 09:43:07 -08:00
Daniel Imms f84384c83a Fix lint 2026-01-03 09:31:45 -08:00
Daniel Imms ec03109c39 2 space indent 2026-01-03 09:25:39 -08:00
Daniel Imms 21b8f77afb Move unit tests to right folder, push font fixtures 2026-01-03 09:16:58 -08:00
Daniel Imms 363fc84fd2 Merge font-ligatures into addon-ligatures
Fixes #5568
2026-01-03 09:02:06 -08:00
Daniel Imms 8e4de23c11 Merge pull request #5565 from Tyriar/2797
Support ctrl+^ -> RS, move to physical key
2026-01-01 05:25:52 -08:00
Daniel Imms 1caf6dbe50 Fix tests 2026-01-01 05:19:56 -08:00
Daniel Imms 1c97e40a6a Support ctrl+^ -> RS, move to physical key
Fixes #2797
Part of #1862
2026-01-01 04:41:44 -08:00
Daniel Imms cdbafb231d Merge pull request #5564 from Tyriar/3364
Serialize showCursor
2025-12-31 17:14:02 -08:00
Daniel Imms 44a8c7efbc Fix test expectations 2025-12-31 17:08:05 -08:00
Daniel Imms 6277bb363c Serialize showCursor
Fixes #3364
2025-12-31 08:12:39 -08:00
Daniel Imms 40f2eefec7 Merge pull request #5563 from Tyriar/5489
Fix touch scrolling
2025-12-31 06:33:36 -08:00
Daniel Imms 519b1c5ef7 Fix touch scrolling
Fixes #5489
2025-12-31 06:27:45 -08:00
Daniel Imms 97e356fb83 Merge pull request #5562 from Tyriar/2117_3
Add tests for CSI Pm d, e, f, g, m
2025-12-31 05:24:41 -08:00
Daniel Imms 42872a84dd Add tests for CSI Pm d, e, f, g, m
Fixes #2117
2025-12-31 05:18:17 -08:00
Daniel Imms 4700e18833 Merge pull request #5561 from Tyriar/5558
Fix allowTransparency in lib and demo
2025-12-31 05:08:29 -08:00
Daniel Imms d143340b60 Fix allowTransparency in lib and demo
Fixes #5558
Fixes #5560
2025-12-31 05:00:33 -08:00
Daniel Imms a8f26bb758 Merge pull request #5559 from Tyriar/2117_2
Add tests for CSI `Ps K`, `? Ps K`, `Ps S`, `Ps T`, `Ps X`, `Ps Z`, `Ps ^`, `Ps \``
2025-12-30 07:06:37 -08:00
Daniel Imms da4243cf02 Don't skip new tests, implement CSI Ps ^ 2025-12-30 06:59:27 -08:00
Daniel Imms 54e66f3ac4 Add some more CSI tests 2025-12-30 06:48:28 -08:00
Daniel Imms 7d8ef60860 Merge pull request #5557 from Tyriar/playwright_update2
Update playwright test to latest
2025-12-30 06:47:24 -08:00
Daniel Imms 595c119404 Move skip into dom file 2025-12-30 06:05:46 -08:00
Daniel Imms 481431fbc1 Update playwright test to latest 2025-12-30 06:03:17 -08:00
Daniel Imms 5c2cd474ce Merge pull request #5556 from Tyriar/5555
Migrate evaluate calls to use terminal proxy
2025-12-30 05:43:14 -08:00
Daniel Imms 0b9a7cb856 Migrate MouseTracking and Terminal to playwright proxy
Fixes #5555
2025-12-30 05:37:09 -08:00
Daniel Imms a8a4455189 Migrate InputHandler.test.ts to playwright proxy
Part of #5555
2025-12-30 05:27:28 -08:00
Daniel Imms 11fbdb3db7 Merge pull request #5554 from Tyriar/2117
Add unit test for CSI Ps SP @ - SL
2025-12-30 05:10:44 -08:00
Daniel Imms 88ce0d40e7 Add unit test for CSI Ps SP @ - SL
Part of #2117
2025-12-30 05:04:20 -08:00
Daniel Imms 2785f11860 Merge pull request #5553 from Tyriar/1526
Save cursor and restore more modes
2025-12-30 04:55:51 -08:00
Daniel Imms 857c4529cd Save cursor and restore more modes
Fixes #1526
2025-12-30 04:49:54 -08:00
Daniel Imms d30c3e7204 Merge pull request #5552 from Tyriar/contrib
Rework contributing doc
2025-12-30 04:42:18 -08:00
Daniel Imms 982f5e15ff Merge pull request #5551 from Tyriar/702
Add dimensions API
2025-12-30 04:37:31 -08:00
Daniel Imms ec3e4f2af1 Rework contributing doc
Fixes #5550
2025-12-30 04:33:57 -08:00
Daniel Imms 13914ee01e Remove private use in ImageRenderer 2025-12-30 04:09:56 -08:00
Daniel Imms e27cc2c089 Remove private API access from tests/demo 2025-12-30 04:06:26 -08:00
Daniel Imms 214289aaab Add dimensions API
Fixes #702
2025-12-30 03:58:26 -08:00
Daniel Imms a5af434e3e Merge pull request #5549 from Tyriar/5548
Use OffscreenCanvas over DOM for WidthCache
2025-12-30 03:38:15 -08:00
Daniel Imms a513120348 Get dom/width cache tests passing in node 2025-12-30 03:32:34 -08:00
Daniel Imms ef978b370b Use OffscreenCanvas over DOM for WidthCache
Fixes #5548
2025-12-30 02:57:09 -08:00
Daniel Imms 00ad3eac40 Merge pull request #5547 from Tyriar/3433
Serialize scroll region
2025-12-29 07:59:49 -08:00
Daniel Imms 29d3eb2e52 Serialize scroll region
fixes #3433
2025-12-29 07:53:43 -08:00
Daniel Imms 8b594140e9 Merge pull request #5546 from Tyriar/progress
Add progress indicator custom glyphs (EE00-EE0B)
2025-12-29 07:37:53 -08:00
Daniel Imms 7cf48c1eb5 Add progress indicator custom glyphs (EE00-EE0B)
Fixes #5478
2025-12-29 07:31:52 -08:00
Daniel Imms 8e6b6e7464 Merge pull request #5545 from Tyriar/nf
Add nerd fonts test button
2025-12-29 06:39:13 -08:00
Daniel Imms 1b0554714b Add nerd fonts test button
Fixes #5544
2025-12-29 06:33:27 -08:00
Daniel Imms abdfa2628c Merge pull request #5543 from Tyriar/underline_inspect
Report underline attributes in demo cell inspector
2025-12-29 06:23:38 -08:00
Daniel Imms 5b4d794f7a Report underline attributes in demo cell inspector 2025-12-29 06:17:36 -08:00
Daniel Imms 9eb1da79e3 Merge pull request #5542 from Tyriar/4064
Make curly underline more vivid
2025-12-29 06:14:01 -08:00
Daniel Imms d4ba4c109b Make curly underline more vivid
Fixes #4064
2025-12-29 06:04:29 -08:00
Daniel Imms 1ad23d6081 Merge pull request #5539 from Tyriar/ris
Add RIS metadata comment
2025-12-29 05:06:07 -08:00
Daniel Imms 73c01cae89 Merge pull request #5541 from Tyriar/4114
Fix fit using correct dims on demo
2025-12-29 05:03:58 -08:00
Daniel Imms 5174a7310f Add RIS metadata comment
Fixes #5043
2025-12-29 05:00:13 -08:00
Daniel Imms 1d793c9054 Fix fit using correct dims on demo
This doesn't add the debounce suggested in the issue, but it does
comment about it.

Fixes #4114
2025-12-29 04:58:12 -08:00
Daniel Imms 1b7c61a6fc Merge pull request #5540 from Tyriar/4855
Fix decorations with height > 1
2025-12-29 04:20:51 -08:00
Daniel Imms 9780b44115 Fix decorations with height > 1
Fixes #4855
2025-12-29 04:14:56 -08:00
Daniel Imms 8d532a35c4 Merge pull request #5538 from Tyriar/4731_mobile_warn
Update viewport dims when dpr changes
2025-12-28 16:36:30 -08:00
Daniel Imms d6df8d7902 Update viewport dims when dpr changes
Fixes #4731
2025-12-28 16:30:44 -08:00
Daniel Imms 4b9566cef0 Merge pull request #5535 from Tyriar/5408_2
Fix firefox selection when in shadow dom take 2
2025-12-28 16:05:49 -08:00
Daniel Imms b30bb1e3b0 Fix stack overflow 2025-12-28 15:59:29 -08:00
Loïc Mangeonjean d67e73a4da fix(fit-addon): call getComputedStyle on the right window element 2025-12-28 15:55:21 -08:00
Loïc Mangeonjean e7434a932d fix: fix computed style computation on firefox inside a shadow dom 2025-12-28 15:55:21 -08:00
Daniel Imms cdb0497d0d Merge pull request #5534 from Tyriar/5184_sync_reset
Force reset to use sync render
2025-12-28 15:22:13 -08:00
Daniel Imms 2fb42c554e Force reset to use sync render
Fixes #5184
2025-12-28 15:16:23 -08:00
Daniel Imms 3632496c94 Merge pull request #5533 from Tyriar/5532
Fix addon build issue on macOS
2025-12-28 15:08:12 -08:00
Daniel Imms a6407fb3c6 Fix addon build issue on macOS
Fixes #5532
2025-12-28 15:02:16 -08:00
Daniel Imms 65aa28464c Merge pull request #5530 from Tyriar/4339
Fix double annoucne of too much output
2025-12-28 14:57:09 -08:00
Daniel Imms 2baad39ccf Fix double annoucne of too much output
Fixes #4339
2025-12-28 14:51:12 -08:00
Daniel Imms c975956f2a Merge pull request #5529 from Tyriar/worktree1
Force a sync render after resize occurs
2025-12-28 14:44:44 -08:00
Daniel Imms 4619a755b1 Force a sync render after resize occurs
Fixes #4922
Related microsoft/vscode#230120
2025-12-28 14:38:42 -08:00
Daniel Imms bc795d36c0 Merge pull request #5528 from Tyriar/4528
Serialize rich underline styles
2025-12-28 14:26:35 -08:00
Daniel Imms b989c5a568 Fix lint 2025-12-28 14:20:55 -08:00
Daniel Imms d66214b25e Serialize rich underline styles 2025-12-28 14:16:53 -08:00
Daniel Imms 6157f523a8 Merge pull request #5523 from Tyriar/4628
Add idle timeout which pauses cursor blink
2025-12-28 14:04:49 -08:00
Daniel Imms 3ab93ea083 Merge pull request #5526 from Tyriar/lint
Remove eol-last eslint rule
2025-12-28 14:04:18 -08:00
Daniel Imms 7ef4c55ca9 Merge pull request #5525 from Tyriar/demo_hover
Implement cell inspector in demo
2025-12-28 13:57:35 -08:00
Daniel Imms d393154271 Remove eol-last eslint rule
This one's just troublesome when working with generated code. It
doesn't make a difference in the end anyway.
2025-12-28 13:56:22 -08:00
Daniel Imms 2c32ef5963 Fix lint 2025-12-28 13:53:03 -08:00
Daniel Imms a730533790 Implement cell inspector in demo
Fixes #5524
2025-12-28 13:50:37 -08:00
Daniel Imms 8a5ffc1bb1 Tidy comments 2025-12-28 13:42:22 -08:00
Daniel Imms a966f7d591 Explain idle in cursorBlink option 2025-12-28 13:41:21 -08:00
Daniel Imms 4985c9f3f9 Use shared constant for idle timeout 2025-12-28 13:37:25 -08:00
Daniel Imms 2e1651badd Pull dom renderer blink management into class 2025-12-28 13:35:57 -08:00
Daniel Imms 7d1f74fb8b Use const enums for constants 2025-12-28 13:33:51 -08:00
Daniel Imms f8c1f5a32f Add idle timeout which pauses cursor blink
Fixes #4628
2025-12-28 13:30:58 -08:00
Daniel Imms 79944f7524 Merge pull request #5521 from Tyriar/5314
Add quirks setting to control DECSET/DECRST 12
2025-12-28 08:07:47 -08:00
Daniel Imms 52b6710b19 Hide quirks from demo for now 2025-12-28 08:01:56 -08:00
Daniel Imms 2a283a307c Fix lint 2025-12-28 07:56:31 -08:00
Daniel Imms ab4524873b Add test for DECSET quirks 2025-12-28 07:49:12 -08:00
Daniel Imms eb5f0d709a Merge pull request #5522 from Tyriar/5295
Note in API that reflowCursorLine will not move the cursor
2025-12-28 07:43:50 -08:00
Daniel Imms 8a8f262616 Note in API that reflowCursorLine will not move the cursor
Fixes #5295
2025-12-28 07:37:50 -08:00
Daniel Imms ca5a681363 Add quirks setting to control DECSET/DECRST 12
Fixes #5314
2025-12-28 07:34:55 -08:00
Daniel Imms fb10cd62c9 Merge pull request #5520 from Tyriar/4961
Ignore soft hyphen
2025-12-27 14:57:12 -08:00
Daniel Imms 28b62a5e0b Ignore soft hyphen
Fixes #4961
2025-12-27 14:51:07 -08:00
Daniel Imms bedf7e8326 Merge pull request #5519 from Tyriar/5444
Add onBeforeSearch and onAfterSearch APIs
2025-12-27 14:38:30 -08:00
Daniel Imms f50028abce Add onBeforeSearch and onAfterSearch APIs
Fixes #5444
2025-12-27 14:32:39 -08:00
Daniel Imms 7a4c9cadff Merge pull request #5518 from Tyriar/style
Demo: Improve style, remove headers from windows
2025-12-27 09:03:02 -08:00
Daniel Imms 01c1c477c3 Demo: Improve style, remove headers from windows 2025-12-27 08:57:26 -08:00
Daniel Imms 33daadb4ea Merge pull request #5516 from Tyriar/activeMatchBg
Ensure activeMatchBackground decoration is on top
2025-12-27 08:04:50 -08:00
Daniel Imms ef6013aac9 Merge pull request #5517 from Tyriar/demo_addons
Demo: Add addon tabs, shuffle order
2025-12-27 08:04:31 -08:00
Daniel Imms 82f24ad5a5 Demo: Add addon tabs, shuffle order 2025-12-27 06:47:17 -08:00
Daniel Imms 97c435862b Merge pull request #5439 from nya3jp/keydup
Fix key duplication on Android with physical keyboards
2025-12-27 06:38:28 -08:00
Daniel Imms 6488e839a1 Merge branch 'master' into keydup 2025-12-27 06:32:16 -08:00
Daniel Imms 5d3564bb38 Ensure activeMatchBackground decoration is on top
Fixes #5369
2025-12-27 06:30:02 -08:00
Daniel Imms 50985d30f9 Merge pull request #5515 from Tyriar/5457_ctrl+slash
Add ctrl+/ -> ^_ keybinding
2025-12-27 06:05:48 -08:00
Daniel Imms 733e48ec73 Add ctrl+/ -> ^_ keybinding
Fixes #5457
2025-12-27 05:57:43 -08:00
Daniel Imms 057fefd39e Merge pull request #5514 from Tyriar/demo6
Ensure types are checked in demo, fix errors, emit to out-demo
2025-12-27 05:40:41 -08:00
Daniel Imms 4d48f8624b Ensure types are checked in demo, fix errors, emit to out-demo
Part of #5509
2025-12-27 05:31:21 -08:00
Daniel Imms 995b491a7a Merge pull request #5513 from Tyriar/esbuild_warn
Set esbuild log level to warning
2025-12-27 05:23:44 -08:00
Daniel Imms 4fe118a8ff Set esbuild log level to warning
This suppresses a safe to ignore debug log in server task
2025-12-27 05:18:00 -08:00
Daniel Imms 45375c94e2 Merge pull request #5512 from Tyriar/demo_server
Move server.js to TypeScript
2025-12-27 05:12:14 -08:00
Daniel Imms f83760d027 Add esbuild server build task 2025-12-27 04:57:55 -08:00
Daniel Imms 1a1bdefc83 Move server to TS
Fixes #5509
2025-12-27 04:36:18 -08:00
Daniel Imms e28db9e8a9 Merge pull request #5511 from Tyriar/settings
Simplify vscode settings
2025-12-27 04:09:31 -08:00
Daniel Imms be5f510bfb Merge pull request #5510 from Tyriar/dev
Make default build task use a single terminal via concurrently
2025-12-27 04:07:16 -08:00
Daniel Imms ac6b8ebb10 Simplify vscode settings 2025-12-27 04:03:24 -08:00
Daniel Imms 1ada355048 Make default build task use a single terminal via concurrently 2025-12-27 03:41:47 -08:00
Daniel Imms 4ce3e3106b Merge pull request #5508 from Tyriar/demo4
Fix css not being used in demo
2025-12-27 03:15:58 -08:00
Daniel Imms 942aff6af7 Merge pull request #5507 from Tyriar/ts5.9
Upgrade to typescript 5.9.3
2025-12-27 03:14:38 -08:00
Daniel Imms 635b898dc0 Fix css not being used in demo 2025-12-27 03:09:39 -08:00
Daniel Imms e01d1f2f17 Upgrade to typescript 5.9.3 2025-12-27 03:06:14 -08:00
Daniel Imms bd16eeed90 Merge pull request #5506 from Tyriar/shared_compile
Fix type error in SharedRendererTests.ts
2025-12-27 03:01:27 -08:00
Daniel Imms df8f9fc519 Merge pull request #5505 from Tyriar/canvasExceptions
Remove unused skip canvas exceptions from playwright
2025-12-27 02:59:27 -08:00
Daniel Imms 373bf513f1 Fix type error in SharedRendererTests.ts 2025-12-27 02:55:43 -08:00
Daniel Imms 25a5497329 Remove unused skip canvas exceptions from playwright 2025-12-27 02:53:28 -08:00
Daniel Imms 4406043e6b Merge pull request #5504 from Tyriar/onRender_headless
Implement onRender in xterm-headless
2025-12-26 11:33:01 -08:00
Daniel Imms 82f961f76f Move new onRender behavior into headless Terminal impl 2025-12-26 11:27:38 -08:00
Daniel Imms a26471b9f3 Implement onRender in xterm-headless
This effectively enabled implementing an efficient renderer on top
of xterm-headless. See #5392
2025-12-26 11:19:59 -08:00
Daniel Imms ba989a6e6c Merge pull request #5503 from Tyriar/copilot_approve
Add some npm scripts that copilot can approve
2025-12-26 11:16:36 -08:00
Daniel Imms d4f725c649 Merge pull request #5502 from Tyriar/xtversion
Support reporting XTVERSION
2025-12-26 11:13:53 -08:00
Daniel Imms 79f5198b2d Add some npm scripts that copilot can approve 2025-12-26 11:10:53 -08:00
Daniel Imms d8a93b757a Support reporting XTVERSION
Fixes #5342
2025-12-26 11:06:55 -08:00
Daniel Imms 9453634f56 Merge pull request #5501 from Tyriar/demo3
Make clear css file is for index.html
2025-12-26 10:59:43 -08:00
Daniel Imms 10e12cf743 Make clear css file is for index.html
Follow up on #5500
2025-12-26 10:54:07 -08:00
Daniel Imms 79621a4e3f Merge pull request #5500 from Tyriar/demo2
More demo refactors
2025-12-26 10:51:49 -08:00
Daniel Imms 546c5e081d Inline simplified style to test.html 2025-12-26 10:46:27 -08:00
Daniel Imms e6ad59b1b1 Disable grid layout in test.html 2025-12-26 10:38:39 -08:00
Daniel Imms 8eab98b78d Change banner bg color 2025-12-26 04:41:05 -08:00
Daniel Imms 0ed6fd0db6 Fix order of init 2025-12-26 04:40:22 -08:00
Daniel Imms e3f87ab8a1 More move handlers into addons and test window 2025-12-26 04:36:51 -08:00
Daniel Imms 8119bb0a4a Simplify window interaction 2025-12-26 04:31:06 -08:00
Daniel Imms dc0d7c81d9 Add terminal and addon refs to all windows 2025-12-26 04:24:07 -08:00
Daniel Imms fa87ecdc4e Move more handlers into testWindow.ts 2025-12-26 04:15:13 -08:00
Daniel Imms b82e8f345f Move more handlers out, addon types into shared file 2025-12-26 04:09:24 -08:00
Daniel Imms 02e9e56eef Move more handlers into testWindow.ts 2025-12-26 04:04:08 -08:00
Daniel Imms 4fffb96f25 Move addCjk into window, move window interactions out of createTerminal 2025-12-26 03:51:01 -08:00
Daniel Imms 6fe5a124ca Merge pull request #5498 from Tyriar/demo
Improve demo
2025-12-26 03:29:03 -08:00
Daniel Imms f62043de91 Merge pull request #5497 from xtermjs/Tyriar-patch-1
Box Elements -> Block Elements
2025-12-25 04:27:12 -08:00
Daniel Imms 55e5ec6037 Box Elements -> Block Elements 2025-12-25 04:21:47 -08:00
Daniel Imms 7b2a5cb50f Merge pull request #5496 from xtermjs/Tyriar-patch-1
Replace range separator with hyphen
2025-12-25 04:05:17 -08:00
Daniel Imms 8740dcda4d Replace range separator with hyphen 2025-12-25 03:59:54 -08:00
Daniel Imms c046f5b4b1 More refactors 2025-12-25 03:41:47 -08:00
Daniel Imms 6bde923f07 Register the windows 2025-12-24 17:09:20 -08:00
Daniel Imms 40b2bbef34 Improve demo by making full screen, add control bar 2025-12-24 16:43:35 -08:00
Daniel Imms c04806fc92 Merge pull request #5495 from Tyriar/tyriar/git_branch
Support custom glyphs for Git Branch Symbols (PUA U+F5D0-U+F60D)
2025-12-24 16:42:45 -08:00
Daniel Imms 8b1306f46d More polish 2025-12-24 16:13:11 -08:00
Daniel Imms 6612153c44 Add alignment tests 2025-12-24 16:10:36 -08:00
Daniel Imms 2be529ce85 Polish definitions 2025-12-24 16:05:42 -08:00
Daniel Imms 96aa97ce11 Add extended symbols 2025-12-24 15:51:00 -08:00
Daniel Imms 98f4c3128c Stub second batch of glyphs 2025-12-24 15:35:46 -08:00
Daniel Imms ce1ab6fdc4 Correct order or curved glyphs 2025-12-24 15:31:49 -08:00
Daniel Imms e87e0f5443 Add git branch symbol alignment tests 2025-12-24 15:27:30 -08:00
Daniel Imms 4a8d8bd247 Partially implement git branch symbols
Part of #5477
2025-12-24 15:03:28 -08:00
Daniel Imms edf100a24b Merge pull request #5494 from Tyriar/tyriar/scale
Add char-based custom glyph scaling
2025-12-24 13:23:23 -08:00
Daniel Imms ed2d8b8fdc Merge branch 'master' into tyriar/scale 2025-12-24 12:36:32 -08:00
Daniel Imms faeefb53bf Merge pull request #5481 from BattlefieldDuck/fix/incorrect-theme-reference
Fix incorrect ITheme reference in comments
2025-12-24 12:29:40 -08:00
Daniel Imms 32ff1f2115 Merge pull request #5492 from Tyriar/sinon
Remove need for sinon
2025-12-24 12:25:41 -08:00
Daniel Imms acb59daf33 Fix lint 2025-12-24 12:20:12 -08:00
Daniel Imms a3d6bf6710 Merge pull request #5493 from Tyriar/tyriar/node
Upgrade missed parts from node 18 -> 22
2025-12-24 12:10:26 -08:00
Daniel Imms 5e5c623d8b Create test ts project in ligatures addon 2025-12-24 12:09:28 -08:00
Daniel Imms 168520f727 Upgrade missed parts from node 18 -> 22 2025-12-24 11:39:10 -08:00
Daniel Imms 23f61911e2 Remove need for sinon 2025-12-24 11:35:20 -08:00
Daniel Imms 491fa5586a Merge pull request #5491 from Tyriar/jsdom
Upgrade jsdom
2025-12-24 11:33:33 -08:00
Daniel Imms 2f549d74ae Merge pull request #5490 from Tyriar/lint
Update eslint and nyc
2025-12-24 11:26:35 -08:00
Daniel Imms 9df2430433 Upgrade jsdom 2025-12-24 11:26:19 -08:00
Daniel Imms ce9f6275ed Fix lint-api 2025-12-24 11:19:06 -08:00
Daniel Imms 9ca04799fe Merge remote-tracking branch 'origin/master' into lint 2025-12-24 11:16:01 -08:00
Daniel Imms 83757d1bf0 Polish arrow strokes 2025-12-24 11:13:58 -08:00
Daniel Imms 60fa9122a2 Remove old yarn rule and regen package lock 2025-12-24 10:46:59 -08:00
Daniel Imms a8d40a1edf Update eslint and nyc 2025-12-24 10:44:46 -08:00
Daniel Imms 78cc07f88a Split arrow parts into hybrid scaling 2025-12-24 10:30:34 -08:00
Daniel Imms 72d7c69a29 Add char-based custom glyph scaling
Draw calls can now be scaled by the char bounding box instead of the
cell bounding box. This means these character won't be stretched. Both
can also be used at the same like as is the case with 1FBBC RIGHT OPEN
SQUARED DOT.

Fixes #5488
2025-12-24 10:19:11 -08:00
Daniel Imms 0e0b445784 Merge pull request #5487 from Tyriar/tyriar/5480
Move customGlyphs option into webgl addon
2025-12-24 08:46:27 -08:00
Daniel Imms 5d24866042 Move customGlyphs option into webgl addon
Fixes #5480
2025-12-24 08:35:06 -08:00
Daniel Imms cbea462737 Merge pull request #5486 from Tyriar/tyriar/powerline_extra
Implement remaining powerline extra symbols
2025-12-24 08:23:39 -08:00
Daniel Imms a3ae94af1d Correct range 2025-12-24 08:18:19 -08:00
Daniel Imms 0e35521810 Add powerline alignment tests 2025-12-24 08:15:50 -08:00
Daniel Imms 40590fedfa Merge branch 'master' into tyriar/powerline_extra 2025-12-24 08:07:37 -08:00
Daniel Imms 1551d94de4 Implement remaining powerline extra symbols
- Uses fontforge + helper bin/convert_svg_to_custom_glyph.js
- Some of these are very large. They'll be much smaller after gzip but
  if this becomes a problem we can compile the svgs into a binary format

Fixes #5479
2025-12-24 08:06:20 -08:00
Daniel Imms d605803866 Merge pull request #5485 from Tyriar/tyriar/braille
Support custom gylphs for braille patterns (U+2800-U+28FF)
2025-12-24 07:09:40 -08:00
Daniel Imms a797852dfd Support custom gylphs for braille patterns (U+2800-U+28FF)
Fixes #5476
2025-12-24 07:00:53 -08:00
Daniel Imms d42f2b54ff Merge pull request #5484 from Tyriar/tyriar/fix_sextants
Fix sextant rendering
2025-12-24 06:43:57 -08:00
Daniel Imms 79084c3066 Fix sextant rendering
Broke in #5483
2025-12-24 06:38:21 -08:00
Daniel Imms b4f4946cca Merge pull request #5483 from Tyriar/glyph_cleanup
Consolidate custom glyph definition parts
2025-12-24 06:21:11 -08:00
Daniel Imms ed779b9062 Replace PATH_FUNCTION_WITH_WEIGHT with a generic strokeWidth prop 2025-12-24 06:14:41 -08:00
Daniel Imms 20fdfba16f Replace BLOCK_PATTERN_WITH_REGION with new clipPath 2025-12-24 04:33:32 -08:00
Daniel Imms 11dae84ff7 Support clip path on any draw definition 2025-12-24 04:25:59 -08:00
Daniel Imms 579573e77d Merge pull request #5482 from xtermjs/Tyriar-patch-1
Remove line ending editorconfig file
2025-12-24 04:24:04 -08:00
Daniel Imms 2114abe88c Support multiple glyphs with multiple draw calls 2025-12-24 04:19:14 -08:00
Daniel Imms d4fd96639f Remove line ending editorconfig file
This is normalized by git and ends up causing git status to show files that don't actually need to be committed after line ending normalization.
2025-12-24 04:18:31 -08:00
Daniel Imms 269d8c20ae Merge pull request #5475 from Tyriar/depc
Remove remnants of deprecated options
2025-12-24 04:03:49 -08:00
TatLead 49b49627f1 Fix incorrect ITheme reference in comments 2025-12-24 07:10:12 +08:00
Daniel Imms 663b94011a Remove remnants of deprecated options
Following up on #5462
2025-12-23 08:24:35 -08:00
Daniel Imms 54fcb1ea2f Merge pull request #5474 from Tyriar/demo_warn
Fix warnings in demo
2025-12-23 08:17:29 -08:00
Daniel Imms a9da3fbf05 Merge pull request #5473 from Tyriar/tyriar/5466_legacy_computing
Support entire Symbols for Legacy Computing unicode block (0x1FB00-0x1FBFF), add missing powerline symbol
2025-12-23 08:15:05 -08:00
Daniel Imms 091c3967f7 Fix warnings in demo 2025-12-23 08:12:12 -08:00
Daniel Imms 10d6ce3805 Update customGlyphs option 2025-12-23 08:09:46 -08:00
Daniel Imms eb285af11d Add diagonal alignment tests 2025-12-23 08:05:02 -08:00
Daniel Imms 0976935f87 Implement remaining Terminal graphic characters (1FBC0-1FBCA) 2025-12-23 07:42:57 -08:00
Daniel Imms 884b667f4e Implement stick figures 1FBC5-1FBC9 2025-12-23 07:04:19 -08:00
Daniel Imms b8843732bf Implement Terminal graphic characters (1FBCB-1FBCD) 2025-12-23 04:49:46 -08:00
Daniel Imms 44d8d2c873 Finish implementing Terminal graphic characters (1FBB9-1FBBC) 2025-12-23 04:28:45 -08:00
Daniel Imms 3d3abf9cf2 Implement folder 1FBB9-1FBBA 2025-12-23 04:22:19 -08:00
Daniel Imms 268f7af5db Implement running man 1FBB2 and 1FBB3 2025-12-23 04:15:41 -08:00
Daniel Imms 16b72dfd63 Mark powerline symbol range as reserved 2025-12-23 03:50:34 -08:00
Daniel Imms a0e658a18c Add column number powerline symbol 2025-12-23 03:42:41 -08:00
Daniel Imms ea6385f310 Comment alignment 2025-12-23 03:39:37 -08:00
Daniel Imms 02a9d2b047 Indicate reserved codepoints in demo 2025-12-23 03:30:02 -08:00
Daniel Imms 5c0c553936 Implement Arrows (1FBB4-1FBB8) 2025-12-22 19:46:35 -08:00
Daniel Imms eb743e0e19 Implement Terminal graphic character (1FBFA-1FBFA) 2025-12-22 19:15:00 -08:00
Daniel Imms 29d91fe264 Fill in remaining parts of legacy computing 2025-12-22 16:27:41 -08:00
Daniel Imms e8dcf46f39 Add PATH_NEGATIVE, simplify 1FBBD-1FBBF 2025-12-22 16:22:06 -08:00
Daniel Imms 66c1316f87 Implement ARROWHEAD-SHAPED POINTER 2025-12-22 15:43:53 -08:00
Daniel Imms e1803ed5a5 Implement Triangular shade characters (1FB9C-1FB9F) 2025-12-22 15:30:55 -08:00
Daniel Imms 1c3813f3d4 Implement Negative terminal graphic characters (1FBBD-1FBBF) 2025-12-22 15:28:51 -08:00
Daniel Imms e12aef9287 Implement Block elements (1FBCE-1FBCF) 2025-12-22 15:21:10 -08:00
Daniel Imms f5edba0b64 Improve docs on segmentedDigit 2025-12-22 15:18:42 -08:00
Daniel Imms 5a995e5595 Implement Geometric shapes (1FBE0-1FBEF) 2025-12-22 15:14:40 -08:00
Daniel Imms 2bae212f83 Implement Character cell diagonals (1FBD0-1FBDF) 2025-12-22 14:19:02 -08:00
Daniel Imms bde3680cd1 Implement 2 more parts 2025-12-22 14:15:26 -08:00
Daniel Imms 717399ceb6 Implement diagonal fill chars and light solid line with stroke 2025-12-22 14:04:51 -08:00
Daniel Imms 51121f1913 Stub legacy computing custom glyph ranges
Part of #5466
2025-12-22 13:42:38 -08:00
Daniel Imms 483acd3335 Merge pull request #5472 from Tyriar/tyriar/5466_refactor__unified
Custom glyph: Unify rendering function and move all definitions into a single ordered object
2025-12-22 13:31:36 -08:00
Daniel Imms 9dc8cecea3 Fix imports 2025-12-22 13:04:44 -08:00
Daniel Imms 89c2649504 Merge branch 'master' into tyriar/5466_refactor__unified 2025-12-22 12:57:03 -08:00
Daniel Imms 390076fc3f Order box drawing consistently 2025-12-22 12:56:16 -08:00
Daniel Imms a4edeed85d Add box drawing to custom glyph ranges test 2025-12-22 12:48:23 -08:00
Daniel Imms e61697fc86 Start organizing box drawing characters 2025-12-22 12:46:43 -08:00
Daniel Imms 9c019b44c3 unified -> customGlyph 2025-12-22 12:39:31 -08:00
Daniel Imms 5579d83f7d Move box drawing into unified 2025-12-22 12:39:14 -08:00
Daniel Imms 429065487a Move block elements and like into unified 2025-12-22 12:34:12 -08:00
Daniel Imms 219036cb0f Add powerline to glyph ranges test 2025-12-22 12:26:58 -08:00
Daniel Imms 3e80637145 Move powerline to unified 2025-12-22 12:14:50 -08:00
Daniel Imms 0af884d379 Correct shade rectangle rendering 2025-12-22 12:09:41 -08:00
Daniel Imms 5ae9ddf4b8 Add range to headers in ranges test 2025-12-22 12:06:59 -08:00
Daniel Imms 7ac068844c Fix warnings in unicodeTables.ts 2025-12-22 12:06:27 -08:00
Daniel Imms 5bf3d57d16 Move rectangular shade chars into unified 2025-12-22 12:05:47 -08:00
Daniel Imms 6461829411 Clean up symbols for legacy def 2025-12-22 11:48:45 -08:00
Daniel Imms efd6a52033 Support path without function 2025-12-22 11:46:53 -08:00
Daniel Imms 3d9e976cb3 Move main symbols for legacy computing into unified 2025-12-22 11:41:49 -08:00
Daniel Imms 415de67106 Start move to unified draw definition
Part of #5466
2025-12-22 11:35:41 -08:00
Daniel Imms c17c01086f Merge pull request #5471 from Tyriar/tyriar/5466_refactor
Refactor custom glyph to move into folder, split definition from raster
2025-12-22 11:34:43 -08:00
Daniel Imms ce41b175ee Remove cyclic dep 2025-12-22 11:11:50 -08:00
Daniel Imms 32b4547c5a Tweak symbol names 2025-12-22 11:09:02 -08:00
Daniel Imms b64543aa59 Make medium shade consistent, make all shades less heavy 2025-12-22 10:57:59 -08:00
Daniel Imms f049c42acc Add CustomGlyph to exports from rasterizer 2025-12-22 10:48:50 -08:00
Daniel Imms 28760cf482 Move custom glyphs into new folder 2025-12-22 10:47:35 -08:00
Daniel Imms 4982305b51 Split definitions and rasterizer 2025-12-22 10:46:53 -08:00
Daniel Imms 7fee40ddcd Merge pull request #5470 from Tyriar/tyriar/5466_shade
Custom glyph support for rectangular shade/combo chars
2025-12-22 10:41:54 -08:00
Daniel Imms 180183e876 Custom glyph support for rectangular shade/combo chars
Range: U+1FB8C-U+1FB94

Part of #5466
2025-12-22 10:29:22 -08:00
Daniel Imms 8308ede3f5 Merge pull request #5468 from Tyriar/5466_mosaic
Custom glyph support for mosaic terminal graphics chars
2025-12-22 10:27:53 -08:00
Daniel Imms a6db7b41f6 Suppress naming-convention for file as it conflicts with unicode props 2025-12-22 08:34:43 -08:00
Daniel Imms f11241d9de Custom glyph support for mosaic terminal graphics chars
Range: U+1FB3C-U+1FB6F

Part of #5466
2025-12-22 08:29:03 -08:00
Daniel Imms 937deb9f4b Merge pull request #5467 from Tyriar/5464_block_sextants
Custom glyph support for block sextants (U+1FB00-U+1FB3B)
2025-12-22 08:27:29 -08:00
Daniel Imms 6acc861b06 Lint 2025-12-22 07:54:47 -08:00
Daniel Imms 8f05b1854b Lint 2025-12-22 07:41:18 -08:00
Daniel Imms 0f461e7a3e Custom glyph support for block sextants (U+1FB00-U+1FB3B)
Fixes #5464
2025-12-22 07:33:43 -08:00
Daniel Imms 4237d82923 Merge pull request #5465 from Tyriar/5464_demo_helper
Add demo helper to print Symbols for Legacy Computing
2025-12-22 07:26:42 -08:00
Daniel Imms 187156764a Add demo helper to print Symbols for Legacy Computing
Range: 1FB00–1FBFF

Part of #5464
2025-12-22 07:16:22 -08:00
Daniel Imms f447274f43 Merge pull request #5463 from Tyriar/tyriar/v6
v6.0.0, bump addon versions, publish unicode graphemes
2025-12-22 05:48:49 -08:00
Daniel Imms f68d1e5d58 Skip sync output tests for now 2025-12-21 13:03:25 -08:00
Daniel Imms 30691e8b46 v6.0.0, bump addon versions, publish unicode graphemes
Part of #5147
2025-12-21 12:49:37 -08:00
Daniel Imms 5fa632518e Merge pull request #5462 from Tyriar/tyriar/deprecated
Remove deprecated windowsMode and fastScrollModifier
2025-12-21 05:06:56 -08:00
Daniel Imms 3571d1f51a Remove windowsMode in favor of windowsPty 2025-12-21 05:01:15 -08:00
Daniel Imms ebc0d83d8e Remove deprecated/unused fastScrollModifier 2025-12-21 04:58:45 -08:00
Daniel Imms d1c50c14e3 Merge pull request #5461 from Tyriar/tyriar/glob
Remove need for glob module
2025-12-20 08:44:33 -08:00
Daniel Imms 92b43bdf9b Remove only 2025-12-20 08:33:56 -08:00
Daniel Imms a9d3ca19f4 Merge pull request #5449 from iSuslov/exports/esm-headless-node
Configure "exports" field in package.json [headless]
2025-12-20 07:47:58 -08:00
Daniel Imms cb4d1ef2c0 Merge pull request #5453 from chrislloyd/feat/synchronized-output
Add synchronized output support (DEC mode 2026)
2025-12-20 07:44:32 -08:00
Daniel Imms 34fd0dcf53 Remove need for glob module 2025-12-20 07:44:24 -08:00
Daniel Imms 7335f15943 Merge pull request #5460 from Tyriar/tyriar/test-results
Git ignore test-results/
2025-12-20 07:37:43 -08:00
Daniel Imms 5e9cd6208f Git ignore test-results/ 2025-12-20 07:37:20 -08:00
Daniel Imms 6733e13a44 Invert initial assertion 2025-12-20 07:33:18 -08:00
Daniel Imms ad07f2cf76 Revert last frame request cache clearing 2025-12-20 07:23:20 -08:00
Daniel Imms 5396243bce Merge pull request #5450 from iSuslov/fix-esm-path-addon-unicode-graphemes
Fix wrong export path for ESM target in [addon-unicode-graphemes]
2025-12-20 07:20:08 -08:00
Daniel Imms 12c4f9536e Use underline cursor style to ensure cursor color isn't reported 2025-12-20 07:13:41 -08:00
Daniel Imms e9ba31f1b6 Explicitly set background color in playwright tests 2025-12-20 06:40:16 -08:00
Daniel Imms ed0ab5f9e4 Add missing property to headless API test assertion 2025-12-20 06:39:27 -08:00
Daniel Imms 0741f8be5c Merge pull request #5452 from iSuslov/upgrade-glob-version-fix-tsc-build
Fix tsc build - upgrade glob version
2025-12-20 06:37:06 -08:00
Daniel Imms 2edd121f1b Add node about serialize addon support 2025-12-20 06:17:31 -08:00
Daniel Imms 1eee5cdf18 Add synchronizedOutput mode to headless terminal object 2025-12-20 06:14:45 -08:00
Daniel Imms 20c1674d00 Update package-lock.json 2025-12-20 06:12:41 -08:00
Daniel Imms 32553fff06 Add synchronizedOutputMode to xterm-headless.d.ts 2025-12-20 06:05:36 -08:00
Daniel Imms 832aa54478 Fix colors and clear cached frames to pass tests 2025-12-20 06:04:11 -08:00
Daniel Imms 7f50b56ae4 Fix one playwright test 2025-12-10 08:44:13 -08:00
Daniel Imms bd361af105 Fix lint 2025-12-10 07:59:52 -08:00
Daniel Imms 9ea059de6f Move SynchronizedOutputHandler to bottom of file 2025-12-10 07:50:59 -08:00
Daniel Imms 7cdf3f9dd7 Prefer const enum so the number is inlined 2025-12-10 07:50:28 -08:00
Daniel Imms 7f81df5a3a Add BSU and ESU to VT tab in demo 2025-12-10 07:49:07 -08:00
Chris LloydandClaude af5dbfcd2c Add DECRQM support for synchronized output mode
Applications can now detect synchronized output support by querying
CSI ? 2026 $ p and receiving CSI ? 2026 ; <mode> $ y response.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 08:26:04 -08:00
Chris LloydandClaude 5d0eae8576 Fix spec URL to use master branch
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 08:23:37 -08:00
Chris LloydandClaude 6b63c5f260 Address PR review feedback for synchronized output
- Remove synchronizedOutputTimeout public API, hardcode 1s timeout
- Extract SynchronizedOutputHandler class for cleaner code
- Update spec URL to contour-terminal/vt-extensions
- Remove unnecessary comment in InputHandler
- Delete unit tests, add integration tests in SharedRendererTests
- Fix whitespace issue

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-09 07:58:31 -08:00
Chris LloydandClaude 7228e2e2dc Add synchronized output support (DEC mode 2026)
Implement synchronized output mode (CSI ? 2026 h/l) which allows
applications to batch terminal updates and render them atomically,
preventing screen tearing during rapid output.

Features:
- BSU (CSI ? 2026 h) pauses rendering, buffering row updates
- ESU (CSI ? 2026 l) flushes buffer and renders atomically
- Configurable timeout via synchronizedOutputTimeout option (default 5s)
- Exposed via terminal.modes.synchronizedOutputMode

Closes #3375

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 11:16:25 -08:00
Ivan Suslov a3e1ecf91b Fix tsc build - upgrade glob version 2025-12-04 07:28:40 -05:00
Ivan Suslov b41d3ef130 Fix ESM export path for addon-unicode-graphemes 2025-12-04 05:02:12 -05:00
Ivan Suslov 8338418471 Added exports field to headless package.json 2025-12-04 03:56:32 -05:00
Daniel Imms 46c510a76d Merge pull request #5413 from afska/patch-1
Adding EmuDevz to the "Real-world uses" list
2025-11-23 13:47:40 -08:00
Daniel Imms 80c3164070 Merge pull request #5446 from Tyriar/tyriar/npm
Migrate from yarn to npm, node 18 -> 22, use npm workspaces
2025-11-23 13:47:15 -08:00
Daniel Imms 236004ce6c Merge branch 'master' into tyriar/npm 2025-11-23 12:53:21 -08:00
Daniel Imms 741d9c3273 Remove install-addon calls 2025-11-23 10:00:21 -08:00
Daniel Imms bf4fb3ad64 Migrate to npm workspaces 2025-11-23 09:54:17 -08:00
Daniel Imms da4ab8a01e Remove peer dependency, generate lock files 2025-11-23 09:35:43 -08:00
Daniel Imms 92eddf8a61 Move to npm 2025-11-23 09:01:24 -08:00
Daniel Imms 637231e6e1 Merge pull request #5445 from Tyriar/tyriar/perf
Prefer performance.now() over Date.now()
2025-11-23 08:46:24 -08:00
Daniel Imms 6468a78237 Fix comment lint 2025-11-23 08:40:35 -08:00
Daniel Imms 98e234992b Prefer performance.now() over Date.now()
performance.now() is faster than Date.now() as there's no need to
consult the system clock. The more important part of this is that
performance.now() is monotonic and is guaranteed to never to backwards.
2025-11-23 08:32:35 -08:00
Daniel Imms e1023d7ec2 Merge pull request #5442 from Tyriar/tyriar/5441
Fix headless module path
2025-11-14 10:18:17 -08:00
Daniel Imms d186a3a115 Fix headless module path
Fixes #5441
2025-11-12 09:30:11 -08:00
Shuhei Takahashi d03416467d Fix key duplication on Android with physical keyboards
When typing rapidly on Android with physical keyboards, characters often
duplicate on the terminal. This is because physical keyboard events
always go through IME on Android, even for plain English characters, and
rapid typing causes a race condition in the composition helper.

This patch fixes the issue by making sure textarea changes are handled
correctly.
2025-11-07 12:33:59 +09:00
Daniel Imms 65a0e03b66 Merge pull request #5437 from xtermjs/anthonykim1/preventPageScrollWhenMouseOff
Prevent entire page from scrolling when scrolling in alt buffer with mouse event off
2025-11-02 03:54:46 -08:00
Anthony Kim 6ae020f51e add this.cancel(ev, true) when line is zero on wheel handler 2025-10-31 14:39:42 -07:00
Daniel Imms 6c79a46513 Merge pull request #5436 from xtermjs/merogge/serialize-range
add range to IHTMLSerializeOptions
2025-10-28 09:41:17 -07:00
meganrogge 4a62856153 fix test 2025-10-28 12:30:54 -04:00
meganrogge 5121867bbb rm line 2025-10-28 11:56:56 -04:00
meganrogge 5a467da137 pass in startLine, endLine, startCol 2025-10-28 11:46:41 -04:00
meganrogge 18bafaf9c9 fix indentation 2025-10-28 10:30:22 -04:00
meganrogge 5141a91bba add range to IHTMLSerializeOptions 2025-10-28 10:27:00 -04:00
Daniel Imms 9fce067266 Merge pull request #5425 from xtermjs/anthonykim1/parsingDataCharCode
Only show char codes for parsing data on trace
2025-10-22 19:08:47 -07:00
Daniel Imms 59241e467c Update src/common/InputHandler.ts 2025-10-22 19:02:14 -07:00
Anthony Kim 09e26980d3 I dont think we need () => in front of typeof data === 'string' 2025-10-22 16:23:35 -07:00
Anthony Kim c3bb36bad9 if trace, debug each AND remove array.from 2025-10-22 16:11:22 -07:00
Anthony Kim 2c57b3219b Only show char codes for parsing data on trace 2025-10-22 09:47:33 -07:00
Daniel Imms e9e8206467 Merge pull request #5423 from Tyriar/tyriar/resize5300
Clear selection on vertical resize
2025-10-19 01:21:28 -07:00
Daniel Imms 9a7517d3eb Clear selection on vertical resize
Fixes #5300
2025-10-19 01:15:30 -07:00
Daniel Imms 1bf90d9f47 Merge pull request #5422 from xtermjs/Tyriar-patch-1
Explain filtering unit tests by file
2025-10-18 13:49:38 -07:00
Daniel Imms 1527e89ad5 Explain filtering unit tests by file 2025-10-18 06:40:45 -07:00
Daniel Imms 95cc31566a Merge pull request #5421 from xtermjs/Tyriar-patch-1
Use Windows PowerShell instead of pwsh 7 in demo
2025-10-18 06:20:41 -07:00
Daniel Imms 7274e1bbea Use Windows PowerShell instead of pwsh 7 in demo
When pwsh 7 isn't installed it's hard to figure out what the problem is.
2025-10-18 06:15:19 -07:00
Daniel Imms a8fabcfa01 Merge pull request #5420 from xtermjs/anthonykim1/charCodesForVerbose
Only log character codes for trace log
2025-10-17 11:39:21 -07:00
Anthony Kim 67cf25989a only send codes, not the data when in trace 2025-10-17 11:26:34 -07:00
Anthony KimandDaniel Imms f87e574971 Update src/common/services/CoreService.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2025-10-17 11:23:24 -07:00
Anthony Kim 5269462817 Only log character codes for verbose/trace 2025-10-17 10:12:10 -07:00
Anthony Kim f4baddaaf1 Merge pull request #5411 from xtermjs/anthonykim1/fixTeleportWhenAltScroll
Fix teleport when exiting out of alt buffer. v2
2025-10-17 09:20:23 -07:00
Rodrigo Alfonso 3aa836e5d7 Adding EmuDevz to the "Real-world uses" list 2025-10-10 03:24:12 -03:00
Anthony Kim 2a6e22e8f8 set this._latestYDisp = undefined onBufferActivate 2025-10-01 20:36:06 -07:00
Anthony Kim 32c3cb01a1 Make sure this._latestYDisp is up to date 2025-09-30 23:04:04 -07:00
Daniel Imms a1d8e96fdd Merge pull request #5406 from Tyriar/tyriar/search_refactor
Refactor search addon into multiple files, add unit tests
2025-09-26 18:24:44 +09:00
Daniel Imms c4780e54fd Merge branch 'master' into tyriar/search_refactor 2025-09-26 17:52:47 +09:00
Daniel Imms c56336b32b Fix unit tests 2025-09-26 17:49:20 +09:00
Daniel Imms d11f411c17 Bring back respecting of cached search term 2025-09-26 17:09:54 +09:00
Daniel Imms f03dfbb3ba Merge pull request #5407 from Tyriar/tyriar/bd09_copilot_instructions
Improve Copilot instructions and add test guidelines
2025-09-26 16:51:46 +09:00
Daniel Imms ffd6dc6ab7 Fix import issue 2025-09-19 09:06:53 -07:00
Daniel Imms 5b4b93acc6 Improve Copilot instructions and add test guidelines
- Enhanced copilot instructions with detailed architecture overview
- Added comprehensive development workflow documentation
- Documented addon development patterns and key examples
- Added project-specific conventions and TypeScript structure
- Included common patterns for parser integration and buffer access
- Added critical implementation details for rendering and events
- Created unit test instructions file with addon testing rules
- Specified preferences for assertions and self-documenting tests
2025-09-19 09:03:02 -07:00
Daniel Imms 2b329eb7b7 Search engine tests 2025-09-19 08:58:30 -07:00
Daniel Imms 9c5644ddc0 Add SearchEngine tests 2025-09-19 08:52:14 -07:00
Daniel Imms 398e87490d Add coding guidelines and lint-fix script
- Add coding guidelines section to copilot instructions
- Specify rule about avoiding comments in unit tests
- Add lint-fix npm script for auto-fixing ESLint issues
2025-09-18 13:27:12 -07:00
Daniel Imms a191370d54 refactor: remove unnecessary interface exports
- Remove export from IHighlight and IMultiHighlight interfaces
- Remove export from ISelectedDecoration interface
- Remove export from ISearchPosition interface
- Keep ISearchResult exported as it's used across multiple files
- Make ISelectedDecoration extend IDisposable for consistency
2025-09-18 13:26:43 -07:00
Daniel Imms bf9ce3bfaf Clean up SearchLineCache.test.ts comments
- Remove unnecessary inline comments from unit tests
- Add descriptive assertion messages where appropriate
- Follow coding guidelines for test comment usage
2025-09-18 13:21:07 -07:00
Daniel Imms ca7e8ca3b7 refactor(addon-search): extract components
- Extract SearchEngine for core search algorithms
- Extract DecorationManager for visual decorations
- Extract SearchState for state management
- Extract SearchResultTracker for result tracking
- Simplify SearchAddon by delegating to components
- Improve modularity and maintainability
2025-09-18 07:38:42 -07:00
Daniel Imms ab43a3bd22 Merge pull request #5405 from Tyriar/tyriar/b8d4_search_interface_refactor
Refactor search addon interfaces
2025-09-18 06:31:18 -07:00
Daniel Imms 32e58c71fe Refactor search addon interfaces
- Move ISearchAddonOptions and ISearchResultChangeEvent to types
- Make noScroll property required in IInternalSearchOptions
- Change exported interfaces to internal scope where appropriate
2025-09-18 06:25:12 -07:00
Daniel Imms b12a9393f1 Merge pull request #5404 from Tyriar/tyriar/1de3_timeout_cleanup
Replace timeout handling with disposableTimeout
2025-09-17 15:12:00 -07:00
Daniel Imms c27af4fa2e Remove thenable from more places 2025-09-17 15:06:33 -07:00
Daniel Imms 90bf20d9d8 Remove Thenable 2025-09-17 15:02:27 -07:00
Daniel Imms 8ffcbd98c4 Replace timeout handling with disposableTimeout
- Use disposableTimeout instead of manual timeout management
- Replace MutableDisposable pattern for SearchAddon highlight timeout
- Replace SearchLineCache timeout with MutableDisposable approach
- Clean up timeout disposal logic in both classes
2025-09-17 14:48:11 -07:00
Daniel Imms 15d90826f9 Merge pull request #5403 from Tyriar/tyriar/76c0_search_line_cache
Add SearchLineCache for improved search performance
2025-09-17 14:38:34 -07:00
Daniel Imms 304143a166 Add SearchLineCache for improved search performance
- Add SearchLineCache class to cache translated buffer lines
- Integrate caching into SearchAddon for line translation
- Improve search performance by avoiding repeated translation
- Add cache invalidation on terminal resize and writes
2025-09-17 14:28:03 -07:00
Daniel Imms 02b3704af0 Merge pull request #5402 from Tyriar/tyriar/9436_copilot_instructions
Update GitHub Copilot instructions with test details
2025-09-17 09:28:07 -07:00
Daniel Imms eb1330ca18 Merge pull request #5401 from Tyriar/tyriar/search-refactor
Refactor search addon types and internal handling
2025-09-17 09:22:07 -07:00
Daniel Imms 3a15c8080d Update GitHub Copilot instructions with test details
- Add comprehensive unit test examples with yarn test-unit
- Include integration test examples with yarn test-integration
- Document test filtering by file path and wildcard patterns
- Explain test runner frameworks (mocha and playwright)
2025-09-17 09:21:40 -07:00
Daniel Imms 6ac9f59ef4 Fix tests 2025-09-17 09:16:20 -07:00
Daniel Imms bffdc59901 Merge branch 'master' into tyriar/search-refactor 2025-09-17 08:53:54 -07:00
Daniel Imms 76a8a8e347 Refactor search addon types and internal handling
- Import search types from addon-search package
- Add IInternalSearchOptions interface for noScroll param
- Update _updateMatches to use internal search methods
- Pass internal options through search method chain
2025-09-17 08:52:38 -07:00
Daniel Imms e37aba5924 Merge pull request #5399 from Tyriar/tyriar/search-constants-refactor
Refactor search constants and improve documentation
2025-09-17 08:16:51 -07:00
Daniel Imms 5c0107462e Merge pull request #5379 from xtermjs/dependabot/npm_and_yarn/addons/addon-ligatures/form-data-4.0.4
Bump form-data from 4.0.0 to 4.0.4 in /addons/addon-ligatures
2025-09-17 08:13:12 -07:00
Daniel Imms 87a45acae7 Refactor search constants and improve documentation
- Convert standalone constants to Constants enum with detailed docs
- Fix spacing issue in SearchAddon class declaration
- Update all references to use Constants enum for consistency
2025-09-17 08:06:56 -07:00
Daniel Imms ab01a5fd65 Merge pull request #5398 from Tyriar/tyriar/search-api-typing
Add proper typing for search results change event
2025-09-17 07:54:29 -07:00
Daniel Imms 6e833e5d5d Add proper typing for search results change event
- Export ISearchResultChangeEvent interface in addon-search
- Use proper Event<T> typing instead of inline object
- Improve API consistency and type safety for consumers
2025-09-17 07:33:43 -07:00
Daniel Imms d7d7f1b023 Merge pull request #5391 from Tyriar/tyriar/mouse_events
Bring back partial wheel tracking
2025-09-09 18:42:38 -07:00
Anthony Kim 9c42fb348b ignore fastScrollModifier 2025-09-09 13:21:37 -07:00
Anthony Kim e4f222f6b0 Make test happy 2025-09-09 12:03:51 -07:00
Anthony Kim 38bee71630 More clear comments 2025-09-09 12:00:19 -07:00
Anthony Kim 1a6a807f68 Remove stale comment 2025-09-09 11:59:29 -07:00
Anthony Kim 03ce26c697 Questions, possible concern 2025-09-09 11:56:28 -07:00
Anthony Kim cf6b868dfb fastScrollModifier seems deprecated 2025-09-09 11:51:27 -07:00
Anthony Kim d862c82386 no stale comments 2025-09-08 21:14:20 -07:00
Anthony Kim 50df7a69e6 remove unused var 2025-09-08 21:06:37 -07:00
Anthony Kim 26ecba8de1 Fix sluggish mouse problem. Now similar to trackpad 2025-09-08 21:03:05 -07:00
Daniel Imms 0d1e6b7a5e Merge pull request #5390 from xtermjs/anthonykim1/fixTeleportAltBuffer
Fix scrollbar teleport after exiting alt buffer
2025-08-29 05:41:52 -07:00
Anthony Kim b104a5b953 Respect original formatting 2025-08-28 22:56:15 -07:00
Anthony Kim ebddd5d346 Move from CoreBrowerTerminal to CoreMouseService. Still slow though 2025-08-28 22:54:50 -07:00
Daniel Imms 5bd7b4aaec Bring back partial wheel tracking
Part of microsoft/vscode#224750
2025-08-28 10:28:19 -07:00
Anthony Kim a6fdf043ce Respect original formatting 2025-08-28 08:57:25 -07:00
Anthony Kim 287702610c More clean up after removing test 2025-08-28 08:54:01 -07:00
Anthony Kim 4d59eca5f9 Clean up 2025-08-28 08:52:46 -07:00
Anthony Kim 753ea81fc0 BufferService listen to onBufferActivate, remove tests 2025-08-28 08:46:27 -07:00
Anthony Kim 675b2c3a70 Improve test 2025-08-27 21:31:34 -07:00
Anthony Kim 86745e1320 Add some tests from copilot 2025-08-27 21:12:15 -07:00
Anthony Kim a3ab942ea1 Fix teleport issue when coming back from alt buffer 2025-08-27 20:45:52 -07:00
Daniel Imms e969832946 Merge pull request #5385 from anthonykim1/anthonykim1/fixWrappedTerminalFind
Fix terminal find when wrapped
2025-08-21 12:40:01 -07:00
Anthony Kim 95a9531ec1 Try to add tests 2025-08-20 22:49:03 -07:00
Daniel Imms 7637a2de19 Merge pull request #5386 from alexkozy/safe-guards
fix: ensure that currentRow can not go out of the range
2025-08-20 14:15:08 -07:00
Alexey Kozy a589be2826 fix: ensure that currentRow can not go out of the range 2025-08-20 13:47:40 -07:00
Anthony Kim b53096f4a2 Clean code 2025-08-20 10:55:47 -07:00
Anthony Kim 687d8277e7 Fix terminal find when wrapped 2025-08-20 10:53:40 -07:00
Daniel Imms 4c0cf27cfc Merge pull request #5334 from CodinGame/webgl-renderer-shadow-dom
Support shadow dom in webgl renderer
2025-07-31 08:32:44 -07:00
Loïc Mangeonjean 149527ddb8 refactor: apply requested style change 2025-07-31 17:08:30 +02:00
dependabot[bot] 9c3eb170d2 Bump form-data from 4.0.0 to 4.0.4 in /addons/addon-ligatures
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.0...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-21 23:07:47 +00:00
Daniel Imms c4b707e9ca Merge pull request #5362 from Tyriar/tyriar/steps2
Move copilot steps to correct folder
2025-06-21 11:01:00 -07:00
Daniel Imms 9d35d59fb3 Move copilot steps to correct folder 2025-06-21 11:00:33 -07:00
Daniel Imms 877b37d3cc Merge pull request #5359 from Tyriar/tyriar/copilot_steps
Add copilot setup steps
2025-06-21 10:55:11 -07:00
Daniel Imms f14f55c951 Add copilot setup steps 2025-06-21 10:54:46 -07:00
Daniel Imms 8a16a551e0 Merge pull request #5358 from Tyriar/tyriar/reflow_bounds
Revert "Merge pull request #5321 from Tyriar/5319"
2025-06-18 06:17:46 -07:00
Daniel Imms 2a5239578a Revert "Merge pull request #5321 from Tyriar/5319"
This reverts commit a260f7d288, reversing
changes made to 2042bb8502.
2025-06-18 06:09:53 -07:00
Daniel Imms d28eb696ec Merge pull request #5355 from Tyriar/tyriar/3634
Add note about reverse tabnapping
2025-06-13 08:41:38 -07:00
Daniel Imms 4c0c2559c3 Add note about reverse tabnapping
Fixes #3634
2025-06-13 08:19:31 -07:00
Daniel Imms ee0a3deb6a Merge pull request #5349 from xtermjs/copilot/fix-3382
Add comprehensive tests for alt+shift+letter keyboard combinations
2025-06-05 03:47:13 -07:00
copilot-swe-agent[bot]andTyriar 076f051bb5 Fix ESLint trailing spaces warning in Keyboard.test.ts
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-05 10:34:53 +00:00
copilot-swe-agent[bot]andTyriar 658c57546c Add comprehensive tests for alt+shift+letter keyboard combinations
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-06-05 10:24:34 +00:00
copilot-swe-agent[bot] 223a650e3e Initial plan for issue 2025-06-05 10:13:25 +00:00
Daniel Imms 4b6316e510 Merge pull request #5346 from xtermjs/copilot/fix-4538-2
Remove alt -> ctrl+arrow hack in favor of embedder-specific solutions
2025-05-31 06:59:41 -07:00
copilot-swe-agent[bot]andTyriar 41546230dc Remove alt -> ctrl+arrow hack and update tests
Co-authored-by: Tyriar <2193314+Tyriar@users.noreply.github.com>
2025-05-29 13:20:54 +00:00
copilot-swe-agent[bot] 8413c0f94b Initial plan for issue 2025-05-29 13:09:01 +00:00
Daniel Imms 8828b47cea Merge pull request #5345 from Tyriar/tyriar/instruct
Add instructions to help Copilot coding agent
2025-05-29 06:05:50 -07:00
Daniel Imms b0f300e61b Add instructions to help Copilot coding agent 2025-05-29 06:00:41 -07:00
Loïc Mangeonjean 3ad83dd70e chore: add webgl shadow dom test 2025-05-21 21:51:01 +02:00
Loïc Mangeonjean 496b53d84d fix: support shadow dom in webgl renderer 2025-05-20 16:31:17 +02:00
Jörg Breitbart 085a275038 fix promise return, reject on missing family name, doc polish 2024-10-06 23:53:30 +02:00
Jörg Breitbart 104c63d7e0 cleanup files 2024-10-04 17:52:45 +02:00
Jörg Breitbart 70c9861c39 Merge branch 'master' into webfonts-addon 2024-10-04 17:51:18 +02:00
Jörg Breitbart c93623cb3b CI tests 2024-10-04 14:24:36 +02:00
Jörg Breitbart b0389a2ed1 future proof demo 2024-10-04 05:31:47 +02:00
Jörg Breitbart 7907f13774 add license notes for fonts 2024-10-04 04:14:10 +02:00
Jörg Breitbart 956c7cbcba demo tests 2024-10-04 04:00:20 +02:00
Jörg Breitbart 23bef99132 cleanup API 2024-10-04 02:06:02 +02:00
Jörg Breitbart b104bcd144 remove await remnants 2024-10-04 00:42:38 +02:00
Jörg Breitbart 809b9aa1ad fix test runner 2024-10-04 00:15:49 +02:00
Jörg Breitbart 9a536f758d fix integration test runner 2024-10-03 22:23:31 +02:00
Jörg Breitbart 15a63bae22 change to global replacement 2024-10-03 22:10:40 +02:00
Jörg Breitbart 41640d9402 make linter happy 2024-10-03 22:07:10 +02:00
Jörg Breitbart 15664de472 initial 2024-10-03 21:50:34 +02:00
695 changed files with 48518 additions and 34348 deletions
+28
View File
@@ -0,0 +1,28 @@
---
name: benchmark
description: Run and author benchmark tests in xterm.js. Use when editing benchmark files, working in `*.benchmark.ts`, or when asked how to run the full benchmark suite, a single benchmark file, or a single benchmark case.
---
# Benchmark Run Instructions
## Running Benchmarks
- Full suite: `npm run benchmark`
- Single benchmark file:
- Tree: `npm run benchmark -- -t out-test/benchmark/Event.benchmark.js`
- Run file: `npm run benchmark -- -s "out-test/benchmark/Event.benchmark.js" out-test/benchmark/Event.benchmark.js`
- Single context/case:
- Use `-t` to get the path, then:
- `npm run benchmark -- -s "<path>" out-test/benchmark/Event.benchmark.js`
## Benchmark Case Selection
When writing benchmark instructions:
- Use `RuntimeCase` to measure pure runtime in ms.
- Use `ThroughputRuntimeCase` when measuring throughput in MB/s.
## Notes
- Benchmarks run from built JS in `out-test/benchmark/*.benchmark.js`.
- Keep `NODE_PATH=./out` (handled by the npm script).
+12
View File
@@ -0,0 +1,12 @@
---
name: unit-test
description: Write and review xterm.js unit tests with project conventions. Use when editing `*.test.ts` files or when asked for test authoring guidelines.
---
# Unit Test Instructions
When writing unit tests, follow these rules:
- When writing unit tests for addons, always create a real xterm.js instance instead of mocking it.
- Prefer `assert.ok` over `assert.notStrictEqual` when checking something is undefined or not.
- Avoid comments as most tests should be self-documenting.
+3 -3
View File
@@ -1,15 +1,15 @@
{
"name": "xterm.js",
"image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
"image": "mcr.microsoft.com/devcontainers/typescript-node:22-bookworm",
"features": {
"ghcr.io/devcontainers/features/node:1": {
"version": 18
"version": 22
} // yarn
},
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install && yarn setup",
"postCreateCommand": "npm install && npm run setup",
"customizations": {
"vscode": {
"extensions": [
-1
View File
@@ -5,7 +5,6 @@ indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.{j,t}s]
max_line_length = 100
-273
View File
@@ -1,273 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
"src/vs/tsconfig.json",
"test/benchmark/tsconfig.json",
"test/playwright/tsconfig.json",
"addons/addon-attach/src/tsconfig.json",
"addons/addon-attach/test/tsconfig.json",
"addons/addon-clipboard/src/tsconfig.json",
"addons/addon-clipboard/test/tsconfig.json",
"addons/addon-fit/src/tsconfig.json",
"addons/addon-fit/test/tsconfig.json",
"addons/addon-image/src/tsconfig.json",
"addons/addon-image/test/tsconfig.json",
"addons/addon-ligatures/src/tsconfig.json",
"addons/addon-progress/src/tsconfig.json",
"addons/addon-progress/test/tsconfig.json",
"addons/addon-search/src/tsconfig.json",
"addons/addon-search/test/tsconfig.json",
"addons/addon-serialize/src/tsconfig.json",
"addons/addon-serialize/test/tsconfig.json",
"addons/addon-serialize/benchmark/tsconfig.json",
"addons/addon-unicode11/src/tsconfig.json",
"addons/addon-unicode11/test/tsconfig.json",
"addons/addon-unicode-graphemes/src/tsconfig.json",
"addons/addon-unicode-graphemes/test/tsconfig.json",
"addons/addon-unicode-graphemes/benchmark/tsconfig.json",
"addons/addon-web-links/src/tsconfig.json",
"addons/addon-web-links/test/tsconfig.json",
"addons/addon-webgl/src/tsconfig.json",
"addons/addon-webgl/test/tsconfig.json"
],
"sourceType": "module"
},
"ignorePatterns": [
"addons/*/src/third-party/*.ts",
"src/vs/*",
"out/*",
"out-test/*",
"out-esbuild/*",
"out-esbuild-test/*",
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js",
"**/*.mjs"
],
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/consistent-type-definitions": "warn",
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/explicit-member-accessibility": [
"warn",
{
"accessibility": "explicit",
"overrides": {
"constructors": "off"
}
}
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"warn",
{ "selector": "default", "format": ["camelCase"],
"filter": {
"regex": "^[a-z]",
"match": true
}
},
// variableLike
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
// memberLike
{ "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"],
"filter": {
"regex": "^[a-z]",
"match": true
}
},
// restrict on* naming for events only
{ "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
} },
// typeLike
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
],
"@typescript-eslint/no-confusing-void-expression": [
"warn",
{ "ignoreArrowShorthand": true }
],
"@typescript-eslint/no-useless-constructor": "warn",
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"curly": [
"warn",
"multi-line"
],
"eol-last": "warn",
"eqeqeq": [
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"jsdoc/check-param-names": 1,
"jsdoc/no-multi-asterisks": 1,
"keyword-spacing": "warn",
"max-len": [
"warn",
{
"code": 1000, // Don't enforce for code
"comments": 100,
"ignoreTrailingComments": true,
"ignoreUrls": true,
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
}
],
"new-parens": "warn",
"no-duplicate-imports": "warn",
"no-else-return": [
"warn",
{
"allowElseIf": false
}
],
"no-eval": "warn",
"no-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-restricted-imports": [
"warn",
{
"patterns": [
".*\\/out\\/.*"
]
}
],
"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."
}
],
"no-trailing-spaces": "warn",
"no-unsafe-finally": "warn",
"no-unused-vars": ["warn", {
"vars": "all",
"args": "none"
}],
"no-var": "warn",
"one-var": [
"warn",
"never"
],
"object-curly-spacing": [
"warn",
"always"
],
"prefer-const": "warn",
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
},
"overrides": [
{
"files": [
"**/*.api.ts",
"**/*.test.ts"
],
"rules": {
"object-curly-spacing": "off",
"max-len": "off",
"no-unused-vars": "off"
}
}
]
}
-107
View File
@@ -1,107 +0,0 @@
{
"env": {
"browser": true,
"es2021": true,
"node": true
},
"parser": "@typescript-eslint/parser",
"plugins": [
"@stylistic/ts",
"@typescript-eslint",
"jsdoc"
],
"rules": {
"@stylistic/ts/indent": [
"warn",
2
],
"@stylistic/ts/semi": [
"warn",
"always"
],
"@stylistic/ts/quotes": [
"warn",
"single",
{ "allowTemplateLiterals": true }
],
"@typescript-eslint/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"@typescript-eslint/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"@typescript-eslint/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@typescript-eslint/naming-convention": [
"warn",
{ "selector": "typeLike", "format": ["PascalCase"] },
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
],
"@typescript-eslint/prefer-namespace-keyword": "warn",
"@typescript-eslint/type-annotation-spacing": "warn",
"comma-dangle": [
"warn",
{
"objects": "never",
"arrays": "never",
"functions": "never"
}
],
"curly": [
"warn",
"multi-line"
],
"eol-last": "warn",
"eqeqeq": [
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"jsdoc/check-param-names": 1,
"keyword-spacing": "warn",
"max-len": [
"warn",
{
"code": 1000, // Don't enforce for code
"comments": 80,
"ignoreUrls": true,
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
}
],
"no-extra-semi": "error",
"no-irregular-whitespace": "warn",
"no-trailing-spaces": "warn",
"object-curly-spacing": [
"warn",
"always"
],
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"],
"exceptions": ["-"]
}
]
}
}
+83 -74
View File
@@ -6,23 +6,28 @@ on:
pull_request:
branches: [ "master" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
node-version: 18.x
cache: 'yarn'
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: yarn --frozen-lockfile
run: npm ci
- name: Setup and run tsc
run: yarn setup
run: npm run setup
- name: Esbuild
run: yarn esbuild
run: npm run esbuild
- name: Zip artifacts
run: |
zip -r compressed-build \
@@ -62,11 +67,14 @@ jobs:
./addons/addon-web-links/lib/* \
./addons/addon-web-links/out/* \
./addons/addon-web-links/out-*/* \
./addons/addon-web-fonts/lib/* \
./addons/addon-web-fonts/out/* \
./addons/addon-web-fonts/out-*/* \
./addons/addon-webgl/lib/* \
./addons/addon-webgl/out/* \
./addons/addon-webgl/out-*st/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
with:
name: build-artifacts
path: compressed-build.zip
@@ -76,39 +84,41 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
node-version: 18.x
cache: 'yarn'
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Lint code
env:
NODE_OPTIONS: --max_old_space_size=4096
run: yarn lint
run: npm run lint
- name: Lint API
run: yarn lint-api
run: npm run lint-api
test-unit-coverage:
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
node-version: 18.x
cache: 'yarn'
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- uses: actions/download-artifact@v4
npm ci
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -122,35 +132,32 @@ jobs:
ls -R
- name: Unit test coverage
run: |
yarn test-unit-coverage --forbid-only
npm run test-unit-coverage --forbid-only
EXIT_CODE=$?
./node_modules/.bin/nyc report --reporter=cobertura
exit $EXIT_CODE
test-unit:
needs: build
timeout-minutes: 20
strategy:
matrix:
node-version: [18]
node-version: [22]
runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v4
npm ci
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -163,35 +170,32 @@ jobs:
fi
ls -R
- name: Unit tests
run: yarn test-unit --forbid-only
run: npm run test-unit --forbid-only
test-integration:
needs: build
timeout-minutes: 20
strategy:
matrix:
node-version: [18] # just one as integration tests are about testing in browser
node-version: [22] # just one as integration tests are about testing in browser
runs-on: [ubuntu-22.04] # macos is flaky
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Install playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -203,53 +207,58 @@ jobs:
unzip -o compressed-build.zip
fi
ls -R
- name: Build demo
run: yarn esbuild-demo
- name: Build demo client
run: npm run esbuild-demo-client
- name: Build demo server
run: npm run esbuild-demo-server
- name: Integration tests (core) # Tests use 50% workers to reduce flakiness
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=core
- name: Integration tests (addon-attach)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-attach
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-attach
- name: Integration tests (addon-clipboard)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-clipboard
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-clipboard
- name: Integration tests (addon-fit)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-fit
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-fit
- name: Integration tests (addon-image)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-image
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-image
- name: Integration tests (addon-progress)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-progress
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-progress
- name: Integration tests (addon-search)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-search
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-search
- name: Integration tests (addon-serialize)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-serialize
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-serialize
- name: Integration tests (addon-unicode-graphemes)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode-graphemes
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-unicode-graphemes
- name: Integration tests (addon-unicode11)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-unicode11
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-unicode11
- name: Integration tests (addon-web-fonts)
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-web-fonts
- name: Integration tests (addon-web-links)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-web-links
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-web-links
- name: Integration tests (addon-webgl)
run: yarn test-integration-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
run: npm run test-integration-${{ matrix.browser }} -- --workers=50% --forbid-only --suite=addon-webgl
release-dry-run:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18]
node-version: [22]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'yarn'
cache: 'npm'
- name: Install dependencies
run: |
yarn --frozen-lockfile
yarn install-addons
npm ci
- name: Install playwright
run: npx playwright install
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -263,7 +272,7 @@ jobs:
ls -R
- name: Package headless
run: |
yarn package-headless
npm run package-headless
node ./bin/package_headless.js
- name: Publish to npm (dry run)
run: node ./bin/publish.js --dry
+11 -4
View File
@@ -8,6 +8,11 @@ on:
schedule:
- cron: '41 17 * * 0'
permissions:
actions: read
contents: read
security-events: write
jobs:
analyze:
name: Analyze
@@ -24,12 +29,14 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4
+37
View File
@@ -0,0 +1,37 @@
name: "Copilot Setup Steps"
on:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
permissions:
contents: read
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Setup and run tsc
run: npm run setup
- name: Esbuild
run: npm run esbuild
+13 -8
View File
@@ -6,23 +6,28 @@ on:
# without running tests to save time to publish
branches: [ "master" ]
permissions:
contents: read
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
node-version: 18.x
cache: 'yarn'
persist-credentials: false
- name: Use Node.js 22.x
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: yarn --frozen-lockfile
run: npm ci
- name: Build
run: yarn setup
run: npm run setup
- name: Package headless
run: |
yarn package-headless
npm run package-headless
node ./bin/package_headless.js
- name: Publish to npm
env:
+8 -1
View File
@@ -3,6 +3,7 @@ node_modules/
.lock-wscript
lib/
out/
out-demo/
out-test/
out-esbuild/
out-esbuild-test/
@@ -16,9 +17,15 @@ docs/
npm-debug.log
/.idea/
.env
mise.toml
build/
.DS_Store
package-lock.json
yarn.lock
test-results/
# Tracing/debug files
.cursor/nal-trace/
.cursor/*.log
# Keep bundled code out of Git
dist/
-7
View File
@@ -1,7 +0,0 @@
Antonis Kalipetis <akalipetis@sourcelair.com>
Antonis Kalipetis <akalipetis@sourcelair.com> <akalipetis@gmail.com>
Daniel Imms <daimms@microsoft.com> <tyriar@tyriar.com>
Paris Kasidiaris <paris@sourcelair.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
Thanasis Daglis <thanasis@sourcelair.com> <ath.daglis@gmail.com>
-1
View File
@@ -1 +0,0 @@
NODE_PATH=./out
-5
View File
@@ -1,5 +0,0 @@
require:
- source-map-support/register
reporter: spec
color: true
check-leaks: true
-1
View File
@@ -1 +0,0 @@
package-lock=false
+1 -1
View File
@@ -1 +1 @@
18
22
+374
View File
@@ -0,0 +1,374 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": [
"import",
"typescript"
],
"categories": {
"correctness": "off"
},
"options": {
"typeAware": true,
"maxWarnings": 0
},
"env": {
"builtin": true
},
"ignorePatterns": [
"addons/*/src/third-party/*.ts",
"**/out/*",
"**/out-test/*",
"**/out-esbuild/*",
"**/out-esbuild-test/*",
"**/inwasm-sdks/*",
"**/typings/*.d.ts",
"**/node_modules",
"**/*.js",
"**/*.mjs"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unassigned-vars": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"preserve-caught-error": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"no-array-constructor": "error",
"no-unused-expressions": "error",
"typescript/ban-ts-comment": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "error",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-misused-new": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error",
"import/no-cycle": "error"
},
"overrides": [
{
"files": [
"**/*.ts",
"**/*.tsx",
"**/*.mts",
"**/*.cts"
],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-var": "error",
"no-with": "off",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error"
}
},
{
"files": [
"**/*.ts"
],
"jsPlugins": [
"@stylistic/eslint-plugin",
{
"name": "jsdoc-js",
"specifier": "eslint-plugin-jsdoc"
},
"oxlint-plugin-eslint"
],
"rules": {
"@stylistic/comma-spacing": [
"warn",
{
"before": false,
"after": true
}
],
"@stylistic/indent": [
"warn",
2
],
"@stylistic/semi": [
"warn",
"always"
],
"@stylistic/quotes": [
"warn",
"single",
{
"allowTemplateLiterals": "always"
}
],
"@stylistic/member-delimiter-style": [
"warn",
{
"multiline": {
"delimiter": "semi",
"requireLast": true
},
"singleline": {
"delimiter": "comma",
"requireLast": false
}
}
],
"@stylistic/type-annotation-spacing": "warn",
"@stylistic/space-unary-ops": "warn",
"@stylistic/keyword-spacing": "warn",
"@stylistic/max-len": [
"warn",
{
"code": 1000,
"comments": 100,
"ignoreTrailingComments": true,
"ignoreUrls": true,
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
}
],
"@stylistic/new-parens": "warn",
"@stylistic/no-extra-semi": "error",
"@stylistic/no-trailing-spaces": "warn",
"eslint-js/object-curly-spacing": [
"warn",
"always"
],
"@stylistic/spaced-comment": [
"warn",
"always",
{
"markers": [
"/"
],
"exceptions": [
"-"
]
}
],
"jsdoc-js/check-alignment": "warn",
"jsdoc-js/check-param-names": "warn",
"jsdoc-js/multiline-blocks": "warn",
"jsdoc-js/no-multi-asterisks": "warn",
"eslint-js/one-var": [
"warn",
"never"
],
"eslint-js/no-restricted-syntax": [
"warn",
{
"selector": "CallExpression[callee.name='requestAnimationFrame']",
"message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression[callee.name='cancelAnimationFrame']",
"message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']",
"message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']",
"message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
},
{
"selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']",
"message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService."
}
],
"curly": [
"warn",
"multi-line"
],
"eqeqeq": [
"warn",
"always"
],
"no-duplicate-imports": "warn",
"no-else-return": [
"warn",
{
"allowElseIf": false
}
],
"no-eval": "warn",
"no-irregular-whitespace": "warn",
"no-restricted-imports": [
"warn",
{
"patterns": [
".*\\/out\\/.*"
]
}
],
"no-unsafe-finally": "warn",
"no-unused-vars": "off",
"no-var": "warn",
"no-empty": "off",
"no-empty-pattern": "off",
"no-cond-assign": "off",
"no-case-declarations": "off",
"for-direction": "off",
"no-prototype-builtins": "off",
"no-useless-escape": "off",
"no-self-assign": "off",
"no-async-promise-executor": "off",
"prefer-rest-params": "off",
"no-control-regex": "off",
"no-fallthrough": "off",
"prefer-spread": "off",
"prefer-const": "warn",
"no-unused-expressions": "off",
"typescript/array-type": [
"warn",
{
"default": "array",
"readonly": "generic"
}
],
"typescript/consistent-type-assertions": "warn",
"typescript/consistent-type-definitions": "warn",
"typescript/explicit-function-return-type": [
"warn",
{
"allowExpressions": true
}
],
"typescript/explicit-member-accessibility": [
"warn",
{
"accessibility": "explicit",
"overrides": {
"constructors": "off"
}
}
],
"typescript/no-confusing-void-expression": [
"warn",
{
"ignoreArrowShorthand": true
}
],
"typescript/no-useless-constructor": "warn",
"typescript/prefer-nullish-coalescing": [
"warn",
{
"ignorePrimitives": true
}
],
"typescript/prefer-namespace-keyword": "warn",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"args": "none"
}
],
"typescript/no-require-imports": "off",
"typescript/no-explicit-any": "off",
"typescript/no-wrapper-object-types": "off",
"typescript/no-empty-object-type": "off",
"typescript/no-unsafe-function-type": "off",
"typescript/no-this-alias": "off",
"typescript/no-namespace": "off",
"typescript/no-duplicate-enum-values": "off"
}
},
{
"files": [
"**/*.api.ts",
"**/*.test.ts"
],
"rules": {
"eslint-js/object-curly-spacing": "off",
"@stylistic/max-len": "off",
"typescript/no-unused-vars": "off",
"typescript/explicit-function-return-type": "off",
"typescript/explicit-member-accessibility": "off"
}
},
{
"files": [
"demo/**/*.ts",
"**/*.benchmark.ts"
],
"rules": {
"typescript/prefer-nullish-coalescing": "off"
}
}
]
}
+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"
}
}
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"recommendations": [
"dbaeumer.vscode-eslint"
]
}
+1 -4
View File
@@ -15,9 +15,6 @@
"--recursive",
"${workspaceRoot}/out/**/*.test.js"
],
"env": {
"NODE_PATH": "${workspaceRoot}/out"
},
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/out/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
@@ -61,7 +58,7 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["start"],
"stopOnEntry": true,
"runtimeVersion": "18",
"runtimeVersion": "22",
"serverReadyAction": {
"action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)"
+1 -20
View File
@@ -1,23 +1,4 @@
{
"files.associations": {
".eslintrc.json.typings": "jsonc"
},
"files.exclude": {
"**/.nyc_output": true
},
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
"mochaExplorer.envPath": ".mocha.env",
"mochaExplorer.files": [
"out/**/*.test.js",
"addons/**/out/*.test.js",
"out-*/**/*.test.js",
"addons/**/out-*/*.test.js"
],
"mochaExplorer.watch": [
"out/**/*.js",
"addons/**/out/*.js",
"out-*/**/*.js",
"addons/**/out-*/*.js"
]
"typescript.preferences.quoteStyle": "single"
}
+25 -4
View File
@@ -10,13 +10,22 @@
"tasks": [
// Compound tasks
{
"label": "Development",
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo"],
"label": "dev",
"detail": "Runs all tasks required to run the demo in a single terminal using concurrently. This does not support problem matching.",
"type": "npm",
"script": "dev",
"isBackground": true,
"group": {
"kind": "build",
"isDefault": true
}
},
{
"label": "dev (separate terminals)",
"detail": "Runs all tasks required to run the demo in separate terminals. This does support problem matching.",
"dependsOn": ["demo-server", "tsc", "esbuild", "esbuild-demo-client", "esbuild-demo-server"],
"group": "build"
},
// Demo
{
@@ -55,9 +64,21 @@
}
},
{
"label": "esbuild-demo",
"label": "esbuild-demo-client",
"type": "npm",
"script": "esbuild-demo-watch",
"script": "esbuild-demo-client-watch",
"dependsOn": ["esbuild", "tsc"],
"group": "build",
"isBackground": true,
"problemMatcher": "$esbuild-watch",
"presentation": {
"group": "xterm-demo"
}
},
{
"label": "esbuild-demo-server",
"type": "npm",
"script": "esbuild-demo-server-watch",
"dependsOn": ["esbuild", "tsc"],
"group": "build",
"isBackground": true,
+113
View File
@@ -0,0 +1,113 @@
# xterm.js Copilot Instructions
## Architecture Overview
**Core Structure**: xterm.js is a multi-target terminal emulator with three main distributions:
- `src/browser/`: Full-featured browser terminal with DOM rendering
- `src/headless/`: Server-side terminal for Node.js (no DOM)
- `src/common/`: Shared core logic (parsing, buffer management, terminal state)
**Key Classes**:
- `Terminal` (browser/headless): Public API wrapper
- `CoreTerminal` (common): Core terminal logic and state
- `CoreBrowserTerminal` (browser): Browser-specific terminal implementation
## Development Workflow
**Build System**:
```bash
npm run build && npm run esbuild # Build all TypeScript and bundle
```
**Testing**:
- Unit tests: `npm run test-unit` (Mocha)
- Unit tests filtering to file: `npm run test-unit -- **/fileName.ts
- Per-addon unit tests: `npm run test-unit -- addons/addon-image/out-esbuild/*.test.js`
- 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
All addons follow this structure:
```typescript
export class MyAddon implements ITerminalAddon {
activate(terminal: Terminal): void {
// Called when loaded via terminal.loadAddon()
// Register handlers, access terminal APIs
}
dispose(): void {
// Cleanup when addon is disposed
}
}
```
**Key Examples**:
- `addons/addon-fit/`: Terminal sizing
- `addons/addon-webgl/`: GPU-accelerated rendering
- `addons/addon-search/`: Text search functionality
## Project-Specific Conventions
**TypeScript Project Structure**: Uses TypeScript project references (`tsconfig.all.json`) for incremental builds across browser/headless/addons.
**API Design**:
- Browser and headless terminals share the same public API
- Proposed APIs require `allowProposedApi: true` option
- Constructor-only options (cols, rows) cannot be changed after instantiation
**Disposable Management**:
- When a disposable object can be replaced over time, prefer a registered `MutableDisposable` over manual dispose/reassign logic.
- Register it on the owning class (for example, `this._register(new MutableDisposable())`) and assign through `.value`; this automatically disposes the previous value and avoids accidentally leaking resources.
**TypeScript Constants**:
- Prefer `const enum` over top-level `const` declarations for primitive constants when appropriate, since values are inlined and avoid runtime property lookups.
**Testing Utilities**: Use `TestUtils.ts` helpers:
- `openTerminal(ctx, options)` for setup
- `pollFor(page, fn, expectedValue)` for async assertions
- `writeSync(page, data)` for terminal input
## Common Patterns
**Parser Integration**: Register custom escape sequence handlers:
```typescript
terminal.parser.registerCsiHandler('m', params => {
// Handle SGR sequences
return true; // Handled
});
```
**Buffer Access**: Read terminal content via buffer API:
```typescript
const line = terminal.buffer.active.getLine(0);
const cell = line?.getCell(0);
```
**Events**: All terminals emit standard events (onData, onResize, onRender) plus platform-specific ones.
## Critical Implementation Details
- Terminal rendering uses either DOM or WebGL renderers
- Buffer lines are immutable; create new instances for modifications
- Character width handling supports Unicode 11+ and grapheme clustering
- Mouse events translate web events to terminal protocols (X10, VT200, etc.)
- Color theming supports both palette and true color modes
## 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.
+35 -89
View File
@@ -1,104 +1,50 @@
# How to contribute to xterm.js
- [Opening issues for bug reports or feature requests](#opening-issues)
- [Contributing code](#contributing-code)
- [Opening issues](#opening-issues)
- [Answering discussion questions](#answering-discussion-questions)
## Contributing code
You can find issues to work on by looking at issues labeled with [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). It's a good idea to comment on the issue saying that you're looking into it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
- Fork [xterm.js](https://github.com/xtermjs/xterm.js/) ([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make the fix and verify it works in the demo. Be sure to follow thew general code style of the rest of the project.
- If your changes are easy to test or likely to regress in the future, add tests. These could be unit or integration (playwright) tests.
- Submit a pull request ([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
> ![TIP]
> Don't put more than one feature or fix in a single pull request. The smaller pull requests are the easier they are to review and merge.
By contributing code to xterm.js you:
- Agree to license the contributed code under xterm.js' [MIT license](LICENSE).
- Confirm that you have the right to contribute and license the code in question. This means that either you hold all rights on the code, or the rights holder has explicitly granted the right to use it like this, through a compatible open source license or through a direct agreement with you.
## Opening issues
The preferred way to report bugs or request features is to use
[GitHub issues](http://github.com/sourcelair/xterm.js/issues). Before
opening an issue, read these pointers.
[GitHub issues](http://github.com/xtermjs/xterm.js/issues).
### Opening issues effectively
### Creating great issues
- Include information about **the browser in which the problem occurred**. Even
if you tested several browsers, and the problem occurred in all of them,
mention this fact in the bug report. Also include browser version numbers and
the operating system that you're on.
- Include information about **the browser in which the problem occurred** or the terminal being used. If you tested several browsers and the problem occurred in all of them, mention this fact in the bug report. Also include browser version numbers and the operating system that you're on.
- Include the version of xterm.js being used, preferably either with the latest `beta` tagged release on npm or reproducing in the demo on the `master` branch.
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the exact steps a maintainer has to take to make the problem occur.
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), provide an HTML document that demonstrates the problem.
- Be polite and follow [the code of conduct](https://github.com/xtermjs/xterm.js?tab=coc-ov-file#readme).
- Mention which release of xterm.js you're using. Preferably, try also with
the current HEAD of the master branch, to ensure the problem has not already been
fixed.
### Issue triaging philosophy
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
exact steps a maintainer has to take to make the problem occur.
It's pretty common for maintainers of large open source projects to suffer from burnout, especially when needing to triage a large number of incoming issues instead of actually building things. Here are some of the steps we take to try mitigate this:
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
bottom of the pile.
- Support questions live in [GH discussions](https://github.com/xtermjs/xterm.js/discussions), issues may be transferred there without further comment and core maintainers may or may not participate in discussions.
- Issues are strictly for well defined features or bugs that are _actionable_.
- Sometimes features are out of scope. A common example of this is a niche feature that the pricipal implementation ([VS Code](https://code.visualstudio.com/)) won't leverage and therefore would be difficult to maintain and likely suffer from bitrot. The reporter may not agree with this, but you could always create an addon if that works or maintain your own fork if it comes to that.
- If a feature does not have a clear way forward or needs more discussion it may be closed ro moved to a discussion.
- If a bug is not easily reproducible it may be closed or moved to a discussion. Generally issues that are labeled are something we want to do or has actionable steps to look into further.
## Answering discussion questions
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
## Contributing code
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
- Make your changes.
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
- Follow the general code style of the rest of the project (see below).
- Submit a pull request
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
Don't put more than one feature/fix in a single pull request.
By contributing code to xterm.js you:
- Agree to license the contributed code under xterm.js' [MIT
license](LICENSE).
- Confirm that you have the right to contribute and license the code
in question. (Either you hold all rights on the code, or the rights
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Test coverage
One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab.
## Testing
### Unit tests
Unit tests are run with `yarn test-unit`:
```sh
# All unit tests
yarn test-unit
# Absolute file path
yarn test-unit out-esbuild/browser/Terminal.test.js
# Filter by wildcard
yarn test-unit out-esbuild/**/Terminal.test.js
# Specific addon unit tests tests
yarn test-unit addons/addon-image/out-esbuild/*.test.js
# Multiple files
yarn test-unit out-esbuild/**/Terminal.test.js out-esbuild/**/InputHandler.test.js
```
These use mocha to run all `.test.js` files within the esbuild output (`out-esbuild/`).
### Integration tests
Integration tests are run with `yarn test-integration`:
```sh
# All integration tests
yarn test-integration
# Core integration tests
yarn test-integration --suite=core
# Specific addon integration tests
yarn test-integration --suite=addon-search
```
These use `@playwright/test` to run all tests within the esbuild test output (`out-esbuild-test/`).
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
+70 -38
View File
@@ -1,30 +1,59 @@
# [![xterm.js logo](images/logo-full.png)](https://xtermjs.org)
# [![xterm.js](images/logo-full.png)](https://xtermjs.org)
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
Xterm.js is a frontend component that enables applications to bring fully-featured terminals to their users in the browser. It's used by popular projects such as [VS Code](https://code.visualstudio.com/) (and its forks), [Tabby](https://tabby.sh/) and [Hyper](https://hyper.is/).
## Features
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Performant**: Xterm.js is *really* fast and includes an optional GPU-accelerated renderer.
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
- **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, addons, well documented API, etc.
- **Self-contained**: The core library has zero dependencies.
- **Accessible**: Screen reader mode and minimum contrast ratio support can be turned on.
- **And much more**: Links, theming, custom glyphs, addons, well documented API, etc.
## What xterm.js is not
- Xterm.js is not a terminal application that you can download and use on your computer.
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output) through a library like [node-pty](https://github.com/microsoft/node-pty).
## Getting Started
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
First, you need to install the module. We ship exclusively through [npm](https://www.npmjs.com), so you need that installed and then add [@xterm/xterm](https://www.npmjs.com/package/@xterm/xterm) as a dependency by running:
```bash
npm install @xterm/xterm
npm install --save @xterm/xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
The recommended way to load xterm.js with the ES module syntax, either using TypeScript or a modern JS tooling. Note that both CommonJS and ES module files are shipped with the npm package.
```javascript
import { Terminal } from '@xterm/xterm';
```
Then instantiate a `Terminal` object, open it on an element and write to it:
```javascript
const term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
```
Most use cases will hook up input and output to a pseudoterminal API such as [node-pty](https://www.npmjs.com/package/node-pty) which will drive the terminal.
```javascript
pty.onData(data => term.write(data));
term.onData(data => pty.write(data));
```
Then make sure to also include the `css/xterm.css` file, for example in HTML:
```html
<link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
```
### Standalone example
Here is a complete standalone example in a HTML file:
```html
<!doctype html>
@@ -36,7 +65,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<body>
<div id="terminal"></div>
<script>
var term = new Terminal();
const term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
@@ -44,22 +73,12 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
</html>
```
### Importing
The recommended way to load xterm.js is via the ES6 module syntax:
```javascript
import { Terminal } from '@xterm/xterm';
```
### Addons
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
```bash
npm i -S @xterm/addon-web-links
npm install --save @xterm/addon-web-links
```
Then import the addon, instantiate it and call `Terminal.loadAddon`:
@@ -80,50 +99,51 @@ The xterm.js team maintains the following addons, but anyone can build them:
- [`@xterm/addon-clipboard`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-clipboard): Access the browser's clipboard
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
- [`@xterm/addon-ligatures`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-ligatures): Enables rendering of ligatures
- [`@xterm/addon-progress`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-progress): Adds support for the progress API (`OSC 9;4`)
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to a VT sequences or HTML
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to VT sequences or HTML
- [`@xterm/addon-unicode-graphemes`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes): Enhanced unicode support including grapheme clustering (experimental)
- [`@xterm/addon-unicode11`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11): Updates character widths to their unicode11 values
- [`@xterm/addon-web-fonts`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-fonts): Easily integrate web fonts
- [`@xterm/addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links): Adds web link detection and interaction
- [`@xterm/addon-webgl`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl): Renders xterm.js using a `canvas` element's webgl2 context
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
Since xterm.js is typically implemented as a developer tool, generally only modern evergreen browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. Xterm.js also works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
### Node.js Support
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection.
We also publish [`@xterm/headless`](https://www.npmjs.com/package/@xterm/headless) which is a stripped down version of xterm.js that runs headless in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the [serialize addon](https://www.npmjs.com/package/@xterm/addon-serialize) so it can get all state restored upon reconnection.
## API
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
Some APIs may be marked with *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
## Releases
Xterm.js follows a monthly release cycle roughly.
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
Stable releases are done on an as needed basis. All current and past releases are available on this repo's [releases page](https://github.com/sourcelair/xterm.js/releases), you can see what's planned for upcoming releases looking through the repository [milestones](https://github.com/sourcelair/xterm.js/milestones).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
Beta releases are continuously published off the `master` branch. Install the latest beta build with:
```bash
npm install -S @xterm/xterm@beta
npm install --save @xterm/xterm@beta
```
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
The principal implementation (VS Code) typically uses the latest or near the latest beta build. Generally they are quite stable but can potentially contain bugs or breaking changes. If stability is very important we recommend using the beta build primarily to test out new features and to verify bug fixes, unless you're tracking what's landing and are comfortable taking that risk.
## Contributing
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
Read [CONTRIBUTING.md](https://github.com/xtermjs/xterm.js/blob/master/CONTRIBUTING.md) to learn how to contribute to the project.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
Xterm.js is used in many world-class applications to provide great terminal experiences.
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
@@ -229,14 +249,26 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**pyTermTk**](https://github.com/ceccopierangiolieugenio/pyTermTk): Python Terminal Toolkit - a Spiced Up Cross Compatible TUI Library 🌶️, use xterm.js for the [HTML5 exporter](https://ceccopierangiolieugenio.github.io/pyTermTk/sandbox/sandbox.html).
- [**ecmaOS**](https://ecmaos.sh): A kernel and suite of applications tying modern web technologies into a browser-based operating system.
- [**LabEx**](https://labex.io): Interactive learning platform with hands-on labs and xterm.js-based online terminals, focused on learn-by-doing approach.
- [**EmuDevz**](https://afska.github.io/emudevz): A free coding game where players learn how to build an emulator from scratch.
- [**SSH Connection Manager**](https://github.com/Amtrend/ssh-manager): Modern web-based SSH terminal and SFTP manager with AES-256 encryption, PWA support, and session management.
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
- [**Cate**](https://github.com/0-AI-UG/cate): Open source desktop IDE on an infinite zoomable canvas. Editor, browser, and terminal panels float in spatial space; the terminals are xterm.js with the WebGL addon, backed by node-pty.
- [**OmnyShell PWA**](https://github.com/OmnyGrid/omnyshell_web): Web client for [**OmnyShell**](https://github.com/OmnyGrid/omnyshell), featuring remote terminals, a built-in IDE, AI-assisted workflows, and detachable/resumable sessions for seamless browser-based access to nodes.
- [**kmux**](https://github.com/kkd927/kmux): A keyboard-centric terminal workspace for running parallel AI coding agents side by side on macOS and Linux.
- [**LynxHub**](https://github.com/KindaBrazy/LynxHub): Cross-platform, extensible terminal and browser for AI management.
- [**Codeman**](https://github.com/Ark0N/Codeman): Self-hosted mission control for AI coding agents, streaming live tmux sessions to browser and phone.
- [**JSVIsion**](https://github.com/blendsdk/jsvision): A Turbo Vision-Style TUI SDK for TypeScript. We use xterm.js in our docs to show live JSVision examples running in a web terminal.
- [**BitFun**](https://github.com/GCWing/BitFun): Open-source desktop AI agent whose integrated terminals and read-only terminal output views use xterm.js with the Fit, Web Links, and WebGL addons.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
## License Agreement
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2017-2026, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-attach",
"version": "0.11.0",
"version": "0.12.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,13 +16,10 @@
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"build": "../../node_modules/.bin/tsgo -p .",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ interface IAttachOptions {
bidirectional?: boolean;
}
export class AttachAddon implements ITerminalAddon , IAttachApi {
export class AttachAddon implements ITerminalAddon, IAttachApi {
private _socket: WebSocket;
private _bidirectional: boolean;
private _disposables: IDisposable[] = [];
+1 -1
View File
@@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => {
});
test.afterAll(async () => await ctx.page.close());
test.describe('Search Tests', () => {
test.describe('AttachAddon', () => {
test.beforeEach(async () => {
await ctx.proxy.reset();
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+3 -1
View File
@@ -9,13 +9,15 @@
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"*": [
"./*"
]
},
"strict": true,
+2 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-clipboard",
"version": "0.1.0",
"version": "0.2.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,16 +16,10 @@
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"build": "../../node_modules/.bin/tsgo -p .",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.4.0"
},
"dependencies": {
"js-base64": "^3.7.5"
}
}
+38 -16
View File
@@ -4,8 +4,7 @@
*/
import type { IDisposable, ITerminalAddon, Terminal } from '@xterm/xterm';
import { type IClipboardProvider, ClipboardSelectionType, type IBase64 } from '@xterm/addon-clipboard';
import { Base64 as JSBase64 } from 'js-base64';
import { type IClipboardProvider, type IBase64 } from '@xterm/addon-clipboard';
export class ClipboardAddon implements ITerminalAddon {
private _terminal?: Terminal;
@@ -25,7 +24,7 @@ export class ClipboardAddon implements ITerminalAddon {
return this._disposable?.dispose();
}
private _readText(sel: ClipboardSelectionType, data: string): void {
private _readText(sel: string, data: string): void {
const b64 = this._base64.encodeText(data);
this._terminal?.input(`\x1b]52;${sel};${b64}\x07`, false);
}
@@ -36,7 +35,7 @@ export class ClipboardAddon implements ITerminalAddon {
return true;
}
const pc = args[0] as ClipboardSelectionType;
const pc = args[0];
const pd = args[1];
if (pd === '?') {
const text = this._provider.readText(pc);
@@ -70,30 +69,53 @@ export class ClipboardAddon implements ITerminalAddon {
}
export class BrowserClipboardProvider implements IClipboardProvider {
public async readText(selection: ClipboardSelectionType): Promise<string> {
if (selection !== 'c') {
return Promise.resolve('');
}
public readText(selection: string): Promise<string> {
return navigator.clipboard.readText();
}
public async writeText(selection: ClipboardSelectionType, text: string): Promise<void> {
if (selection !== 'c') {
return Promise.resolve();
}
public writeText(selection: string, text: string): Promise<void> {
return navigator.clipboard.writeText(text);
}
}
/**
* TODO: Once the base64 handling on Uint8Arrays is more common,
* remove the btoa/atob fallbacks below.
*/
interface IUint8ArrayB64 extends Uint8Array {
toBase64(): string;
}
interface IUint8ArrayB64Ctor extends Uint8ArrayConstructor {
fromBase64(s: string): IUint8ArrayB64;
}
export class Base64 implements IBase64 {
public encodeText(data: string): string {
return JSBase64.encode(data);
const bytes = new TextEncoder().encode(data) as IUint8ArrayB64;
if (bytes.toBase64 !== undefined) {
return bytes.toBase64();
}
let bin = '';
for (let i = 0; i < bytes.length; i++) {
bin += String.fromCharCode(bytes[i]);
}
return btoa(bin);
}
public decodeText(data: string): string {
const text = JSBase64.decode(data);
if (!JSBase64.isValid(data) || JSBase64.encode(text) !== data) {
if ((Uint8Array as IUint8ArrayB64Ctor).fromBase64 !== undefined) {
try {
return new TextDecoder().decode((Uint8Array as IUint8ArrayB64Ctor).fromBase64(data));
} catch {}
return '';
}
return text;
try {
const bin = atob(data);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
bytes[i] = bin.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
} catch {}
return '';
}
}
@@ -20,7 +20,7 @@ test.describe('ClipboardAddon', () => {
test.beforeEach(async ({}, testInfo) => {
// DEBT: This test doesn't work since the migration to @playwright/test
if (ctx.browser.browserType().name() !== 'chromium') {
if (ctx.browser.browserType().name() === 'webkit') {
testInfo.skip();
return;
}
@@ -29,7 +29,7 @@ test.describe('ClipboardAddon', () => {
await ctx.page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
}
await ctx.page.evaluate(`
window.term.reset()
window.term.reset();
window.clipboard?.dispose();
window.clipboard = new ClipboardAddon();
window.term.loadAddon(window.clipboard);
@@ -48,6 +48,14 @@ test.describe('ClipboardAddon', () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('primary selection', async () => {
await ctx.proxy.write(`\x1b]52;p;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('empty selection (default)', async () => {
await ctx.proxy.write(`\x1b]52;;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('invalid base64 string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}invalid\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
@@ -60,19 +68,46 @@ test.describe('ClipboardAddon', () => {
});
test.describe('read data', async function (): Promise<any> {
test('simple string', async () => {
test.beforeAll(async () => {
await ctx.page.evaluate(`
window.data = [];
window.term.onData(e => data.push(e));
`);
});
test('simple string', async () => {
await ctx.page.evaluate('window.data.length = 0;');
await ctx.page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate('window.data'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
test('primary selection', async () => {
await ctx.page.evaluate('window.data.length = 0;');
await ctx.page.evaluate(() => window.navigator.clipboard.writeText('hello world'));
await ctx.proxy.write(`\x1b]52;p;?\x07`);
deepEqual(await ctx.page.evaluate('window.data'), [`\x1b]52;p;${testDataEncoded}\x07`]);
});
test('clear clipboard', async () => {
await ctx.proxy.write(`\x1b]52;c;!\x07`);
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
test.describe('non-ASCII', () => {
const testDataEncoded = '4oKsbWzDpMO8dMOf';
const testDataDecoded = '€mläütß';
test('write simple string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('read simple string', async () => {
await ctx.page.evaluate(`
window.data2 = [];
window.term.onData(e => data2.push(e));
`);
await ctx.page.evaluate((d) => window.navigator.clipboard.writeText(d), testDataDecoded);
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate('window.data2'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
});
});
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+3 -1
View File
@@ -9,13 +9,15 @@
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"*": [
"./*"
]
},
"strict": true,
+12 -21
View File
@@ -30,18 +30,6 @@ declare module '@xterm/addon-clipboard' {
public dispose(): void
}
/**
* Clipboard selection type. This is used to specify which selection buffer to
* read or write to.
* - SYSTEM `c`: The system clipboard.
* - PRIMARY `p`: The primary clipboard. This is provided for compatibility
* with Linux X11.
*/
export const enum ClipboardSelectionType {
SYSTEM = 'c',
PRIMARY = 'p',
}
export interface IBase64 {
/**
* Converts a utf-8 string to a base64 string.
@@ -78,36 +66,39 @@ declare module '@xterm/addon-clipboard' {
export interface IClipboardProvider {
/**
* Gets the clipboard content.
* @param selection The clipboard selection to read.
* @param selection The clipboard selection to read (see OSC 52 spec of xterm).
* @returns A promise that resolves with clipboard selection data.
*/
readText(selection: ClipboardSelectionType): string | Promise<string>;
readText(selection: string): string | Promise<string>;
/**
* Sets the clipboard content.
* @param selection The clipboard selection to set.
* @param selection The clipboard selection to set (see OSC 52 spec of xterm).
* @param data The clipboard text to write.
*/
writeText(selection: ClipboardSelectionType, text: string): void | Promise<void>;
writeText(selection: string, text: string): void | Promise<void>;
}
/**
* The clipboard provider interface that enables xterm.js to access the system clipboard.
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
*
* Note that this clipboard provider does not implement own cut buffers as xterm does,
* but redirect the selection parameter always to navigator.clipboard.
*/
export class BrowserClipboardProvider implements IClipboardProvider{
/**
* Reads text from the clipboard.
* @param selection The selection type to read from.
* @param selection The selection type to read from (always refers to navigator.clipboard).
* @returns A promise that resolves with the text from the clipboard.
*/
public readText(selection: ClipboardSelectionType): Promise<string>;
public readText(selection: string): Promise<string>;
/**
* Writes text to the clipboard.
* @param selection The selection type to write to.
* @param selection The selection type to write to (always refers to navigator.clipboard).
* @param data The text to write to the clipboard.
* @returns A promise that resolves when the text has been written to the clipboard.
*/
public writeText(selection: ClipboardSelectionType, data: string): Promise<void>;
public writeText(selection: string, data: string): Promise<void>;
}
}
-8
View File
@@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
js-base64@^3.7.5:
version "3.7.7"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.7.tgz#e51b84bf78fbf5702b9541e2cb7bfcb893b43e79"
integrity sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==
+2 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-fit",
"version": "0.10.0",
"version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -16,13 +16,10 @@
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"build": "../../node_modules/.bin/tsgo -p .",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
}
}
+33 -29
View File
@@ -3,9 +3,8 @@
* @license MIT
*/
import type { Terminal, ITerminalAddon } from '@xterm/xterm';
import type { Terminal, ITerminalAddon, IRenderDimensions } from '@xterm/xterm';
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ViewportConstants } from 'browser/shared/Constants';
interface ITerminalDimensions {
@@ -20,10 +19,23 @@ interface ITerminalDimensions {
cols: number;
}
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 1;
const enum Constants {
MINIMUM_COLS = 2,
MINIMUM_ROWS = 1
}
export class FitAddon implements ITerminalAddon , IFitApi {
function getWindow(e: Node): Window {
if (e?.ownerDocument?.defaultView) {
return e.ownerDocument.defaultView;
}
return window;
}
function _getComputedStyle(el: HTMLElement): CSSStyleDeclaration {
return getWindow(el).getComputedStyle(el, null);
}
export class FitAddon implements ITerminalAddon, IFitApi {
private _terminal: Terminal | undefined;
public activate(terminal: Terminal): void {
@@ -38,14 +50,7 @@ export class FitAddon implements ITerminalAddon , IFitApi {
return;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
// Force a full render
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
core._renderService.clear();
this._terminal.resize(dims.cols, dims.rows);
}
this._terminal.resize(dims.cols, dims.rows);
}
public proposeDimensions(): ITerminalDimensions | undefined {
@@ -57,35 +62,34 @@ export class FitAddon implements ITerminalAddon , IFitApi {
return undefined;
}
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims: IRenderDimensions = core._renderService.dimensions;
const dims: IRenderDimensions | undefined = this._terminal.dimensions;
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
if (!dims || dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
const scrollbarWidth = (this._terminal.options.scrollback === 0
const showScrollbar = this._terminal.options.scrollbar?.showScrollbar ?? true;
const scrollbarWidth = (this._terminal.options.scrollback === 0 || !showScrollbar
? 0
: (this._terminal.options.overviewRuler?.width || ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
: (this._terminal.options.scrollbar?.width ?? ViewportConstants.DEFAULT_SCROLL_BAR_WIDTH));
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(this._terminal.element);
const parentElementStyle = _getComputedStyle(this._terminal.element.parentElement);
const parentElementHeight = Math.max(0, parseInt(parentElementStyle.getPropertyValue('height'), 10) || 0);
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width'), 10) || 0);
const elementStyle = _getComputedStyle(this._terminal.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
top: parseInt(elementStyle.getPropertyValue('padding-top'), 10) || 0,
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom'), 10) || 0,
right: parseInt(elementStyle.getPropertyValue('padding-right'), 10) || 0,
left: parseInt(elementStyle.getPropertyValue('padding-left'), 10) || 0
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
cols: Math.max(Constants.MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(Constants.MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
+11
View File
@@ -119,6 +119,17 @@ test.describe('FitAddon', () => {
strictEqual(await ctx.proxy.cols, 2);
strictEqual(await ctx.proxy.rows, 1);
});
test('same dimensions', async function(): Promise<any> {
await setDimensions();
await ctx.page.evaluate(`window.fit.fit()`);
const cols: number = await ctx.proxy.cols;
const rows: number = await ctx.proxy.rows;
// Calling fit() again at the same container size should not throw
await ctx.page.evaluate(`window.fit.fit()`);
strictEqual(await ctx.proxy.cols, cols);
strictEqual(await ctx.proxy.rows, rows);
});
});
});
+2 -3
View File
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+3 -1
View File
@@ -9,13 +9,15 @@
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"*": [
"./*"
]
},
"strict": true,
-1
View File
@@ -17,7 +17,6 @@ npm-debug.log
.env
build/
.DS_Store
package-lock.json
yarn.lock
# Keep bundled code out of Git
+40 -42
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)
@@ -25,12 +26,14 @@ const customSettings: IImageAddonOptions = {
pixelLimit: 16777216, // max. pixel size of a single image
sixelSupport: true, // enable sixel support
sixelScrolling: true, // whether to scroll on image output
sixelPaletteLimit: 256, // initial sixel palette size
sixelSizeLimit: 25000000, // size limit of a single sixel sequence
sixelPaletteLimit: 4096, // initial sixel palette size
sixelSizeLimit: 33554432, // size limit of a single sixel sequence
storageLimit: 128, // FIFO storage limit in MB
showPlaceholder: true, // whether to show a placeholder for evicted images
iipSupport: true, // enable iTerm IIP support
iipSizeLimit: 20000000 // size limit of a single IIP sequence
iipSizeLimit: 33554432, // size limit of a single IIP sequence
kittySupport: true, // enable Kitty graphics support
kittySizeLimit: 33554432 // size limit of a single Kitty sequence
}
// initialization
@@ -41,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)
@@ -71,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).
@@ -90,7 +90,7 @@ terminal.loadAddon(imageAddon);
therefore palette animations cannot be used.
- **SIXEL Raster Attributes Handling**
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extend. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extend. Both is discouraged by the >30 years old specification.
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extent. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extent. Both is discouraged by the >30 years old specification.
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
@@ -98,17 +98,31 @@ 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, QOI, WEBP and AVIF
- 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).
- **Note on IIP image format support**
For all image formats except QOI the addon only does the dimension extraction and leaves
the image decoding to the underlying browser engine. Note that some formats are implemented
and enabled with different constraints by browser vendors, especially newer formats
(e.g. under Firefox you might need to change the setting _image.avif.compliance_strictness_ to 0
to get broader AVIF support).
- **Kitty Graphics Support (TGP)**
Set by default, change it with `{kittySupport: true}`.
Note that the kitty graphics support is still WIP.
### Storage and Drawing Settings
@@ -125,6 +139,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:
@@ -138,11 +153,12 @@ 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:
- Incomming image data chunk at `term.write` (terminal)
`term.write` might stock up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
The addon does most image processing in JavaScript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, let's look at the data flow and processing steps:
- Incoming image data chunk at `term.write` (terminal)
`term.write` might stack up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
- Sequence Parser (terminal)
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
- Sequence Handler - Chunk Decoding (addon)
@@ -168,18 +184,18 @@ const totalActive = storageBytes + decodingBytes;
```
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
Further note that the formulas above do not respect the Javascript object's overhead. Compared to the raw buffer needs the book keeping by these objects is rather small (<<5%).
Further note that the formulas above do not respect the JavaScript object's overhead. Compared to the raw buffer needs the bookkeeping by these objects is rather small (<<5%).
_Why should I care about memory usage at all?_
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the Javascript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the JavaScript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
_How can I adjust the memory usage?_
- `pixelLimit`
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage only peaks temporarily, a rather high value should work even with multiple terminals in one page.
- `storageLimit`
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
- `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.
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for peak memory use during decoding.
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
Constructor settings. This has only a small impact on peak memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
### Terminal Interaction
@@ -209,23 +225,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.

After

Width:  |  Height:  |  Size: 123 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-image",
"version": "0.8.0",
"version": "0.9.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -18,16 +18,13 @@
"xterm.js"
],
"scripts": {
"prepackage": "../../node_modules/.bin/tsc -p .",
"prepackage": "../../node_modules/.bin/tsgo -p .",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"start": "node ../../demo/start"
},
"peerDependencies": {
"@xterm/xterm": "^5.2.0"
},
"devDependencies": {
"sixel": "^0.16.0",
"xterm-wasm-parts": "^0.1.0"
"xterm-wasm-parts": "^0.4.1"
}
}
+139 -63
View File
@@ -3,21 +3,27 @@
* @license MIT
*/
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { IIPImageStorage } from './IIPImageStorage';
import { CELL_SIZE_DEFAULT } from './ImageStorage';
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
import QoiDecoder from 'xterm-wasm-parts/lib/qoi/QoiDecoder.wasm';
import { HeaderParser, IHeaderFields, HeaderState, SequenceType } from './IIPHeaderParser';
import { imageType, UNSUPPORTED_TYPE } from './Metrics';
import { Drawable } from './Primitives';
// eslint-disable-next-line
declare const Buffer: any;
// limit hold memory in base64 decoder
const KEEP_DATA = 4194304;
// Local const enum mirror - esbuild can't inline const enums from external packages
const enum DecoderConst {
// Limit held memory in base64 decoder (encoded bytes).
KEEP_DATA = 4194304,
// Initial buffer allocation for the decoder.
INITIAL_DATA = 1048576,
// Local mirror of const enum (esbuild can't inline const enums from external packages)
OK = 0
}
// default IIP header values
const DEFAULT_HEADER: IHeaderFields = {
type: SequenceType.INVALID,
name: 'Unnamed file',
size: 0,
width: 'auto',
@@ -31,22 +37,30 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _aborted = false;
private _hp = new HeaderParser();
private _header: IHeaderFields = DEFAULT_HEADER;
private _dec = new Base64Decoder(KEEP_DATA);
private _metrics = UNSUPPORTED_TYPE;
private _dec: Base64Decoder;
private _qoiDec: QoiDecoder;
private _isMultipart = false;
private _abortMulti = false;
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _renderer: ImageRenderer,
private readonly _storage: ImageStorage,
private readonly _storage: IIPImageStorage,
private readonly _coreTerminal: ITerminalExt
) {}
) {
const maxEncodedBytes = Math.ceil(this._opts.iipSizeLimit * 4 / 3);
const initialBytes = Math.min(DecoderConst.INITIAL_DATA, maxEncodedBytes);
this._dec = new Base64Decoder(DecoderConst.KEEP_DATA, maxEncodedBytes, initialBytes);
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();
}
@@ -54,7 +68,7 @@ export class IIPHandler implements IOscHandler, IResetHandler {
if (this._aborted) return;
if (this._hp.state === HeaderState.END) {
if (this._dec.put(data, start, end)) {
if ((this._dec.put(data.subarray(start, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
}
@@ -65,15 +79,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(this._header.size);
if (this._dec.put(data, dataPos, end)) {
if ((this._dec.put(data.subarray(dataPos, end)) as number) !== DecoderConst.OK) {
this._dec.release();
this._aborted = true;
if (this._isMultipart) this._abortMulti = true;
}
}
}
@@ -82,22 +108,61 @@ export class IIPHandler implements IOscHandler, IResetHandler {
public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted) return true;
let w = 0;
let h = 0;
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
// IMPORTANT: ReportCellSize uses logical points (CSS pixels)
let w = CELL_SIZE_DEFAULT.width;
let h = CELL_SIZE_DEFAULT.height;
const dimensions = this._coreTerminal.dimensions;
if (dimensions) {
w = dimensions.css.canvas.width / this._coreTerminal.cols;
h = dimensions.css.canvas.height / this._coreTerminal.rows;
}
const scale = this._coreTerminal._core._coreBrowserService?.dpr ?? 1;
const report = `\x1b]1337;ReportCellSize=${h.toFixed(3)};${w.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
// early exit condition chain
let cond: number | boolean = true;
let cond: number | boolean;
let metrics = UNSUPPORTED_TYPE;
if (cond = success) {
if (cond = !this._dec.end()) {
this._metrics = imageType(this._dec.data8);
if (cond = this._metrics.mime !== 'unsupported') {
w = this._metrics.width;
h = this._metrics.height;
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;
metrics = imageType(this._dec.data8);
if (cond = metrics.mime !== 'unsupported') {
if (!(cond = metrics.width && metrics.height && metrics.width * metrics.height < this._opts.pixelLimit)) {
console.warn(`IIP: image dimension issue ${metrics.width}x${metrics.height}`);
}
} else {
console.warn('IIP: unsupported image type');
}
} else {
console.warn('IIP: error during BASE64 decoding');
}
}
if (!cond) {
@@ -105,41 +170,52 @@ export class IIPHandler implements IOscHandler, IResetHandler {
return true;
}
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
this._dec.release();
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise<boolean>(r => {
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
const canvas = ImageRenderer.createCanvas(window.document, w, h);
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
this._storage.addImage(canvas);
r(true);
});
img.src = url;
// sanity measure to avoid terminal blocking from dangling promise
// happens from corrupt data (onload never gets fired)
setTimeout(() => r(true), 1000);
});
let bmSrc: Blob | ImageData;
let imgBlob: Blob;
if (metrics.mime === 'image/qoi') {
const data = this._qoiDec.decode(this._dec.data8);
bmSrc = new ImageData(
new Uint8ClampedArray(data.buffer, data.byteOffset, data.byteLength),
this._qoiDec.width,
this._qoiDec.height
);
this._qoiDec.release();
} else {
imgBlob = bmSrc = new Blob([this._dec.data8], { type: metrics.mime });
}
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
this._dec.release();
return createImageBitmap(bmSrc)
.then(bm => {
this._storage.addImage(bm);
const [w, h] = this._resize(metrics.width, metrics.height);
this._storage.addImage(new Drawable(bm), imgBlob, metrics, w / metrics.width, h / metrics.height);
return true;
})
.catch(e => {
console.warn(`IIP: decoding error ${metrics.mime} ${metrics.width}x${metrics.height}`, e);
return true;
});
}
private _resize(w: number, h: number): [number, number] {
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
let cw;
let ch;
let width;
let height;
const dimensions = this._coreTerminal.dimensions;
if (dimensions) {
width = dimensions.device.canvas.width;
height = dimensions.device.canvas.height;
cw = width / this._coreTerminal.cols;
ch = height / this._coreTerminal.rows;
} else {
cw = CELL_SIZE_DEFAULT.width;
ch = CELL_SIZE_DEFAULT.height;
width = cw * this._coreTerminal.cols;
height = ch * this._coreTerminal.rows;
}
const rw = this._dim(this._header.width!, width, cw);
const rh = this._dim(this._header.height!, height, ch);
const rw = this._dim(this._header.width ?? '', width, cw);
const rh = this._dim(this._header.height ?? '', height, ch);
if (!rw && !rh) {
const wf = width / w; // TODO: should this respect initial cursor offset?
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
@@ -154,8 +230,8 @@ export class IIPHandler implements IOscHandler, IResetHandler {
private _dim(s: string, total: number, cdim: number): number {
if (s === 'auto') return 0;
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
return parseInt(s) * cdim;
if (s.endsWith('%')) return parseInt(s.slice(0, -1), 10) * total / 100;
if (s.endsWith('px')) return parseInt(s.slice(0, -2), 10);
return parseInt(s, 10) * cdim;
}
}
+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 });
});
});
+90 -14
View File
@@ -6,8 +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.
@@ -28,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
@@ -81,7 +92,7 @@ function toName(data: Uint32Array): string {
return new TextDecoder().decode(b);
}
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
const DECODERS: {[key: string]: (v: Uint32Array) => number | string} = {
inline: toInt,
size: toInt,
name: toName,
@@ -91,7 +102,19 @@ const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
};
// 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;
@@ -100,7 +123,7 @@ export class HeaderParser {
private _buffer = new Uint32Array(MAX_FIELDCHARS);
private _position = 0;
private _key = '';
public fields: {[key: string]: any} = {};
public fields: {[key: string]: number | string | Uint32Array | null | undefined} = {};
public reset(): void {
this._buffer.fill(0);
@@ -110,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) {
@@ -126,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;
@@ -157,6 +232,7 @@ export class HeaderParser {
}
private _a(): number {
this.fields.type = SequenceType.INVALID;
this.state = HeaderState.ABORT;
return -1;
}
@@ -176,7 +252,7 @@ export class HeaderParser {
try {
const v = this._buffer.slice(0, pos);
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
} catch (e) {
} catch {
return false;
}
return true;
+30
View File
@@ -0,0 +1,30 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IAddImageOpts, IDrawable, IMetrics } from './Types';
import { ImageStorage } from './ImageStorage';
/**
* IIP (iTerm Image Protocol) specific image storage controller.
*
* Wraps the shared ImageStorage with IIP protocol semantics:
* - 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
) {}
/**
* Add an IIP image to storage.
* Always uses scrolling mode — cursor advances past the image.
*/
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, prescaleX: number, prescaleY: number): void {
this._addImageOpts.prescaleX = prescaleX;
this._addImageOpts.prescaleY = prescaleY;
this._storage.addImage(src, data, metrics, this._addImageOpts);
}
}
-76
View File
@@ -1,76 +0,0 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
export interface IMetrics {
mime: ImageType;
width: number;
height: number;
}
export const UNSUPPORTED_TYPE: IMetrics = {
mime: 'unsupported',
width: 0,
height: 0
};
export function imageType(d: Uint8Array): IMetrics {
if (d.length < 24) {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
return UNSUPPORTED_TYPE;
}
function jpgSize(d: Uint8Array): [number, number] {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}
+79 -19
View File
@@ -5,24 +5,72 @@
import type { ITerminalAddon, IDisposable } from '@xterm/xterm';
import type { ImageAddon as IImageApi } from '@xterm/addon-image';
import { Emitter, type IEvent } from 'common/Event';
import { IIPHandler } from './IIPHandler';
import { ImageRenderer } from './ImageRenderer';
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { KittyGraphicsHandler } from './kitty/KittyGraphicsHandler';
import { KittyImageStorage } from './kitty/KittyImageStorage';
import { SixelHandler } from './SixelHandler';
import { SixelImageStorage } from './SixelImageStorage';
import { IIPImageStorage } from './IIPImageStorage';
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
/**
* Document VT features provided by this addon.
*
* @vt: #E[Supported via @xterm/addon-image.] DCS SIXEL "SIXEL Graphics" "DCS Ps ; Ps ; Ps ; q Pt ST" "Draw SIXEL image."
*
* Sixel support is provided by the addon @xterm/addon-image with these limitations:
* - immediate coloring (no shared palette, allows high color settings of `img2sixel`)
* - max. palette size of 4096 colors
* - max. pixel width of 16K
* - max. 25 MB per sixel sequence
* - VT340 cursor positioning (begin of last sixel data row)
*
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image) for more details.
*
*
* @vt: #E[Supported via @xterm/addon-image.] OSC 1337 "iTerm2 Commands" "OSC 1337 ; Pt BEL" "Custom iTerm2 commands."
*
* Only the inline image protocol (IIP) is supported by the addon @xterm/addon-image with
* the following limitations:
* - sequence:
* - format: `OSC 1337 ; File=inline=1 ; size=<unencoded size> ; ... : <base64 payload> BEL`
* - size param must be set and payload may not exceed CEIL(size * 4 / 3)
* - strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding,
* no separator bytes allowed)
* - supported params: size, name, width, height, preserveAspectRatio
* - image formats: PNG, JPEG and GIF
* - no animation support (renders first image of a GIF)
* - no multipart support
* - VT340 cursor positioning (begin of last sixel data row)
*
* See [addon readme](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image)
* and [iTerm2 IIP docs](https://iterm2.com/documentation-images.html) for more details.
*
*
* @vt: #E[Supported via @xterm/addon-image.] APC KITTY_GRAPHICS "Kitty Graphics" "APC G Pt ST" "Kitty Graphics Protocol."
*
* Kitty graphics support is provided by the addon @xterm/addon-image.
* Note that while basic image output already works, this is still work in progress.
*/
// default values of addon ctor options
const DEFAULT_OPTIONS: IImageAddonOptions = {
enableSizeReports: true,
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 256,
sixelSizeLimit: 25000000,
sixelPaletteLimit: 4096,
sixelSizeLimit: 33554432,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
iipSizeLimit: 33554432,
kittySupport: true,
kittySizeLimit: 33554432
};
// max palette size supported by the sixel lib (compile time setting)
@@ -48,7 +96,7 @@ const enum GaStatus {
}
export class ImageAddon implements ITerminalAddon , IImageApi {
export class ImageAddon implements ITerminalAddon, IImageApi {
private _opts: IImageAddonOptions;
private _defaultOpts: IImageAddonOptions;
private _storage: ImageStorage | undefined;
@@ -56,6 +104,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
private _disposables: IDisposable[] = [];
private _terminal: ITerminalExt | undefined;
private _handlers: Map<String, IResetHandler> = new Map();
private readonly _onImageAdded = new Emitter<void>();
public readonly onImageAdded: IEvent<void> = this._onImageAdded.event;
constructor(opts?: Partial<IImageAddonOptions>) {
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
@@ -68,6 +118,7 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
}
this._disposables.length = 0;
this._handlers.clear();
this._onImageAdded.dispose();
}
private _disposeLater(...args: IDisposable[]): void {
@@ -82,21 +133,20 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
// internal data structures
this._renderer = new ImageRenderer(terminal);
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
this._storage.onImageAdded = () => this._onImageAdded.fire();
// 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 || {};
const windowOps = terminal.options.windowOptions ?? {};
windowOps.getWinSizePixels = true;
windowOps.getCellSizePixels = true;
windowOps.getWinSizeChars = true;
terminal.options.windowOptions = windowOps;
}
// apply placeholder setting
this._renderer.showPlaceholder(this._opts.showPlaceholder);
this._disposeLater(
this._renderer,
this._storage,
@@ -129,7 +179,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
// SIXEL handler
if (this._opts.sixelSupport) {
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
const sixelStorage = new SixelImageStorage(this._storage!, this._opts, this._renderer!, terminal);
const sixelHandler = new SixelHandler(this._opts, sixelStorage, terminal);
this._handlers.set('sixel', sixelHandler);
this._disposeLater(
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
@@ -138,12 +189,25 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
// iTerm IIP handler
if (this._opts.iipSupport) {
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
const iipStorage = new IIPImageStorage(this._storage!);
const iipHandler = new IIPHandler(this._opts, iipStorage, terminal);
this._handlers.set('iip', iipHandler);
this._disposeLater(
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
);
}
// Kitty graphics handler
if (this._opts.kittySupport) {
const kittyStorage = new KittyImageStorage(this._storage!);
const kittyHandler = new KittyGraphicsHandler(this._opts, this._renderer!, kittyStorage, terminal);
this._handlers.set('kitty', kittyHandler);
this._disposeLater(
kittyStorage,
kittyHandler,
terminal._core._inputHandler._parser.registerApcHandler({ final: 'G' }, kittyHandler)
);
}
}
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
@@ -189,12 +253,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
return this._storage?.getImageAtBufferCell(x, y);
}
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
return this._storage?.extractTileAtBufferCell(x, y);
}
private _report(s: string): void {
this._terminal?._core.coreService.triggerDataEvent(s);
this._terminal?._core.input(s, false);
}
private _decset(params: (number | number[])[]): boolean {
@@ -283,8 +343,8 @@ export class ImageAddon implements ITerminalAddon , IImageApi {
switch (params[1]) {
// we only implement read and read_max here
case GaAction.READ:
let width = this._renderer?.dimensions?.css.canvas.width;
let height = this._renderer?.dimensions?.css.canvas.height;
let width = this._renderer?.dimensions?.device.canvas.width;
let height = this._renderer?.dimensions?.device.canvas.height;
if (!width || !height) {
// for some reason we have no working image renderer
// --> fallback to default cell size
+184 -243
View File
@@ -3,69 +3,30 @@
* @license MIT
*/
import { toRGBA8888 } from 'sixel/lib/Colors';
import { IDisposable } from '@xterm/xterm';
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import type { ICellSize, ImageLayer, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
import { createCanvas } from './Primitives';
const PLACEHOLDER_LENGTH = 4096;
const PLACEHOLDER_HEIGHT = 24;
/**
* ImageRenderer - terminal frontend extension:
* - provide primitives for canvas, ImageData, Bitmap (static)
* - add canvas layer to DOM (browser only for now)
* - draw image tiles onRender
*/
export class ImageRenderer extends Disposable implements IDisposable {
public canvas: HTMLCanvasElement | undefined;
private _ctx: CanvasRenderingContext2D | null | undefined;
/** @deprecated Kept for backward compat — points to top layer canvas. */
public get canvas(): HTMLCanvasElement | undefined { return this._layers.get('top')?.canvas; }
private _layers = new Map<ImageLayer, CanvasRenderingContext2D>();
private _placeholder: HTMLCanvasElement | undefined;
private _placeholderBitmap: ImageBitmap | undefined;
private _optionsRefresh = this._register(new MutableDisposable());
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
private _renderService: IRenderService | undefined;
private _oldSetRenderer: ((renderer: any) => void) | undefined;
// drawing primitive - canvas
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
/**
* NOTE: We normally dont care, from which document the canvas
* gets created, so we can fall back to global document,
* if the terminal has no document associated yet.
* This way early image loads before calling .open keep working
* (still discouraged though, as the metrics will be screwed up).
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument || document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
return canvas;
}
// drawing primitive - ImageData with optional buffer
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
if (typeof ImageData !== 'function') {
const imgData = ctx.createImageData(width, height);
if (buffer) {
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
}
return imgData;
}
return buffer
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
: new ImageData(width, height);
}
// drawing primitive - ImageBitmap
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
if (typeof createImageBitmap !== 'function') {
return Promise.resolve(undefined);
}
return createImageBitmap(img);
}
// some local variables for faster access
private _dimensions: IRenderDimensions | undefined;
private _cellSize: ICellSize | undefined;
constructor(private _terminal: ITerminalExt) {
super();
@@ -86,6 +47,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
});
this._register(toDisposable(() => {
this.removeLayerFromDom();
this.removeLayerFromDom('bottom');
if (this._terminal._core && this._oldOpen) {
this._terminal._core.open = this._oldOpen;
this._oldOpen = undefined;
@@ -95,10 +57,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
this._oldSetRenderer = undefined;
}
this._renderService = undefined;
this.canvas = undefined;
this._ctx = undefined;
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._layers.clear();
this._placeholder = undefined;
}));
}
@@ -107,15 +66,7 @@ export class ImageRenderer extends Disposable implements IDisposable {
* Enable the placeholder.
*/
public showPlaceholder(value: boolean): void {
if (value) {
if (!this._placeholder && this.cellSize.height !== -1) {
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
}
} else {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
this._placeholder = undefined;
}
this._placeholder = value ? this._createPlaceHolder() : undefined;
this._renderService?.refreshRows(0, this._terminal.rows);
}
@@ -124,176 +75,155 @@ export class ImageRenderer extends Disposable implements IDisposable {
* Forwarded from internal render service.
*/
public get dimensions(): IRenderDimensions | undefined {
return this._renderService?.dimensions;
return this._terminal.dimensions;
}
/**
* Current cell size (float).
* Current cell size.
*/
public get cellSize(): ICellSize {
return {
width: this.dimensions?.css.cell.width || -1,
height: this.dimensions?.css.cell.height || -1
};
public getCellSize(dimensions?: IRenderDimensions): ICellSize | undefined {
dimensions ??= this._terminal.dimensions;
if (dimensions) {
return {
width: dimensions.device.canvas.width / this._terminal.cols,
height: dimensions.device.canvas.height / this._terminal.rows,
};
}
}
/**
* Clear a region of the image layer canvas.
*/
public clearLines(start: number, end: number): void {
this._ctx?.clearRect(
0,
start * (this.dimensions?.css.cell.height || 0),
this.dimensions?.css.canvas.width || 0,
(++end - start) * (this.dimensions?.css.cell.height || 0)
);
public clearLines(start: number, end: number, layer?: ImageLayer): void {
const deviceGrid = this._cellSize;
if (!deviceGrid || !this._dimensions) return;
const y = Math.floor(start * deviceGrid.height);
const w = this._dimensions.device.canvas.width;
const h = Math.ceil((end + 1 - start) * deviceGrid.height);
if (!layer || layer === 'top') {
this._layers.get('top')?.clearRect(0, y, w, h);
}
if (!layer || layer === 'bottom') {
this._layers.get('bottom')?.clearRect(0, y, w, h);
}
}
/**
* Clear whole image canvas.
*/
public clearAll(): void {
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
public clearAll(layer?: ImageLayer): void {
if (!layer || layer === 'top') {
const ctx = this._layers.get('top');
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
if (!layer || layer === 'bottom') {
const ctx = this._layers.get('bottom');
ctx?.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
}
/**
* Draw neighboring tiles on the image layer canvas.
*/
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
if (!this._ctx) {
return;
}
const { width, height } = this.cellSize;
const ctx = this._layers.get(imgSpec.layer);
const initialGrid = imgSpec.cellSize;
const deviceGrid = this._cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
if (!ctx || !imgSpec.src.native || !deviceGrid) {
return;
}
this._rescaleImage(imgSpec, width, height);
const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const img = imgSpec.src;
const cols = Math.ceil(img.width * imgSpec.prescaleX / initialGrid.width);
const sx = (tileId % cols) * width;
const sy = Math.floor(tileId / cols) * height;
const dx = col * width;
const dy = row * height;
const gridWidth = initialGrid.width / imgSpec.prescaleX;
const gridHeight = initialGrid.height / imgSpec.prescaleY;
// safari bug: never access image source out of bounds
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
// crops are initialGrid px
const sx = (tileId % cols) * gridWidth + imgSpec.precropX;
const sy = Math.floor(tileId / cols) * gridHeight + imgSpec.precropY;
const dx = Math.floor(col * deviceGrid.width + imgSpec.offsetX);
const dy = Math.floor(row * deviceGrid.height + imgSpec.offsetY);
// safari bug: never access image source out of bounds, thus we clamp its dimensions
const sWidth = Math.min(count * gridWidth, img.width - sx);
const sHeight = Math.min(gridHeight, img.height - sy);
const dWidth = Math.ceil(sWidth / gridWidth * deviceGrid.width * imgSpec.scaleX);
const dHeight = Math.ceil(sHeight / gridHeight * deviceGrid.height * imgSpec.scaleY);
if (imgSpec.blendMode === 'overwrite') {
ctx.clearRect(dx, dy, dWidth, dHeight);
}
// Floor all pixel offsets to get stable tile mapping without any overflows.
// Note: For not pixel perfect aligned cells like in the DOM renderer
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
this._ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
ctx.drawImage(
img.native,
Math.floor(sx), Math.floor(sy), Math.ceil(sWidth), Math.ceil(sHeight),
dx, dy, dWidth, dHeight
);
}
/**
* Extract a single tile from an image.
*/
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
this._rescaleImage(imgSpec, width, height);
const img = imgSpec.actual!;
const cols = Math.ceil(img.width / width);
const sx = (tileId % cols) * width;
const sy = Math.floor(tileId / cols) * height;
const finalWidth = width + sx > img.width ? img.width - sx : width;
const finalHeight = sy + height > img.height ? img.height - sy : height;
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(
img,
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
);
return canvas;
}
}
/**
* Draw a line with placeholder on the image layer canvas.
*/
public drawPlaceholder(col: number, row: number, count: number = 1): void {
if (this._ctx) {
const { width, height } = this.cellSize;
// Don't try to draw anything, if we cannot get valid renderer metrics.
if (width === -1 || height === -1) {
return;
}
if (!this._placeholder) {
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
} else if (height >= this._placeholder!.height) {
this._createPlaceHolder(height + 1);
}
if (!this._placeholder) return;
this._ctx.drawImage(
this._placeholderBitmap || this._placeholder!,
col * width,
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
width * count,
height,
col * width,
row * height,
width * count,
height
);
const ctx = this._layers.get('top');
const deviceGrid = this._cellSize;
if (!ctx || !deviceGrid) {
return;
}
this._placeholder ??= this._createPlaceHolder();
if (!this._placeholder) return;
ctx.drawImage(
this._placeholder,
0, 0, 1, 1,
col * deviceGrid.width,
row * deviceGrid.height,
count * deviceGrid.width,
deviceGrid.height
);
}
/**
* Rescale image layer canvas if needed.
* Checked once from `ImageStorage.render`.
* NOTE: This method updates dimensions on instance properties
* for faster access during draw and clear calls.
* So make sure to always call this before doing any draws.
*/
public rescaleCanvas(): void {
if (!this.canvas) {
return;
}
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
this.canvas.width = this.dimensions!.css.canvas.width || 0;
this.canvas.height = this.dimensions!.css.canvas.height || 0;
}
}
/**
* Rescale image in storage if needed.
*/
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
return;
}
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
if (currentWidth === originalWidth && currentHeight === originalHeight) {
spec.actual = spec.orig;
spec.actualCellSize.width = originalWidth;
spec.actualCellSize.height = originalHeight;
return;
}
const canvas = ImageRenderer.createCanvas(
this.document,
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
);
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
spec.actual = canvas;
spec.actualCellSize.width = currentWidth;
spec.actualCellSize.height = currentHeight;
public rescaleCanvas(force: boolean = false): void {
const dimensions = this._terminal.dimensions;
if (dimensions) {
const cssW = dimensions.css.canvas.width;
const cssH = dimensions.css.canvas.height;
const devW = dimensions.device.canvas.width;
const devH = dimensions.device.canvas.height;
let recalc = force;
for (const ctx of this._layers.values()) {
if (ctx.canvas.width !== devW
|| ctx.canvas.height !== devH
|| ctx.canvas.style.width !== `${cssW}px`
|| ctx.canvas.style.height !== `${cssH}px`
) {
ctx.canvas.width = devW;
ctx.canvas.height = devH;
ctx.canvas.style.width = `${cssW}px`;
ctx.canvas.style.height = `${cssH}px`;
recalc = true;
}
}
if (recalc) {
this._dimensions = dimensions;
this._cellSize = this.getCellSize(dimensions);
}
} else {
// we were not able to get render dimensions:
// set cellsize to undefined so we don't try to draw anything
this._cellSize = this.getCellSize();
}
}
@@ -304,72 +234,83 @@ export class ImageRenderer extends Disposable implements IDisposable {
this._renderService = this._terminal._core._renderService;
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
this._renderService.setRenderer = (renderer: any) => {
this.removeLayerFromDom();
for (const key of [...this._layers.keys()]) {
this.removeLayerFromDom(key);
}
this._oldSetRenderer?.call(this._renderService, renderer);
};
}
public insertLayerToDom(): void {
public insertLayerToDom(layer: ImageLayer = 'top'): void {
// make sure that the terminal is attached to a document and to DOM
if (this.document && this._terminal._core.screenElement) {
if (!this.canvas) {
this.canvas = ImageRenderer.createCanvas(
this.document, this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0
);
this.canvas.classList.add('xterm-image-layer');
this._terminal._core.screenElement.appendChild(this.canvas);
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
this.clearAll();
}
} else {
if (!this.document || !this._terminal._core.screenElement) {
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
}
}
public removeLayerFromDom(): void {
if (this.canvas) {
this._ctx = undefined;
this.canvas.remove();
this.canvas = undefined;
}
}
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
this._placeholderBitmap?.close();
this._placeholderBitmap = undefined;
// create blueprint to fill placeholder with
const bWidth = 32; // must be 2^n
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
const ctx = blueprint.getContext('2d', { alpha: false });
if (!ctx) return;
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
const d32 = new Uint32Array(imgData.data.buffer);
const black = toRGBA8888(0, 0, 0);
const white = toRGBA8888(255, 255, 255);
d32.fill(black);
for (let y = 0; y < height; ++y) {
const shift = y % 2;
const offset = y * bWidth;
for (let x = 0; x < bWidth; x += 2) {
d32[offset + x + shift] = white;
}
}
ctx.putImageData(imgData, 0, 0);
// create placeholder line, width aligned to blueprint width
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
if (!ctx2) {
this._placeholder = undefined;
return;
}
for (let i = 0; i < width; i += bWidth) {
ctx2.drawImage(blueprint, i, 0);
if (this._layers.has(layer)) {
return;
}
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
const canvas = createCanvas(
this.document,
this.dimensions?.device.canvas.width || 0,
this.dimensions?.device.canvas.height || 0,
this.dimensions?.css.canvas.width || 0,
this.dimensions?.css.canvas.height || 0,
);
canvas.classList.add(`xterm-image-layer-${layer}`);
const screenElement = this._terminal._core.screenElement;
// Use isolation to create a stacking context without overriding z-index,
// which would conflict with integrators (e.g. VS Code) that set their
// own z-index on the screen element.
screenElement.style.isolation = 'isolate';
if (layer === 'bottom') {
// Use z-index:-1 so it paints behind non-positioned text elements.
// The screen element needs to be a stacking context (via isolation)
// to contain the negative z-index, otherwise it would go behind the
// entire terminal.
canvas.style.zIndex = '-1';
screenElement.insertBefore(canvas, screenElement.firstChild);
} else {
// Explicit z-index ensures the image canvas reliably stacks above
// the text layer (DOM renderer rows). z-index: 0 is below the
// selection overlay (z-index: 1).
canvas.style.zIndex = '0';
screenElement.appendChild(canvas);
}
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) {
canvas.remove();
return;
}
this._layers.set(layer, ctx);
// force rescaling to update stored cellSize (maybe not be populated yet)
this.rescaleCanvas(true);
this.clearAll(layer);
}
public removeLayerFromDom(layer: ImageLayer = 'top'): void {
const ctx = this._layers.get(layer);
if (ctx) {
ctx.canvas.remove();
this._layers.delete(layer);
}
}
public hasLayer(layer: ImageLayer): boolean {
return this._layers.has(layer);
}
/**
* Create a semi-transparent gray 1x1 placeholder
*/
private _createPlaceHolder(): HTMLCanvasElement | undefined {
const imgData = new ImageData(1, 1);
new Uint32Array(imgData.data.buffer).fill(128 << 24 | 128 << 16 | 128 << 8 | 128);
const canvas = createCanvas(this.document, 1, 1);
const ctx = canvas.getContext('2d', { alpha: true });
if (!ctx) return;
ctx.putImageData(imgData, 0, 0);
return canvas;
}
public get document(): Document | undefined {
+209 -228
View File
@@ -5,8 +5,14 @@
import { IDisposable } from '@xterm/xterm';
import { ImageRenderer } from './ImageRenderer';
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
import type {
ITerminalExt, IImageAddonOptions, IImageSpec, ICellSize, IAddImageOpts,
IDrawable,
IMetrics
} from './Types';
import type { IBufferLine } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { createCanvas } from './Primitives';
// fallback default cell size
export const CELL_SIZE_DEFAULT: ICellSize = {
@@ -14,93 +20,12 @@ export const CELL_SIZE_DEFAULT: ICellSize = {
height: 14
};
/**
* Extend extended attribute to also hold image tile information.
*
* Object definition is copied from base repo to fully mimick its behavior.
* Image data is added as additional public properties `imageId` and `tileId`.
*/
class ExtendedAttrsImage implements IExtendedAttrsImage {
private _ext: number = 0;
public get ext(): number {
if (this._urlId) {
return (
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
(this.underlineStyle << 26)
);
}
return this._ext;
}
public set ext(value: number) { this._ext = value; }
public get underlineStyle(): UnderlineStyle {
// Always return the URL style if it has one
if (this._urlId) {
return UnderlineStyle.DASHED;
}
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
}
public set underlineStyle(value: UnderlineStyle) {
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
}
public get underlineColor(): number {
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public set underlineColor(value: number) {
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
}
public get underlineVariantOffset(): number {
const val = (this._ext & ExtFlags.VARIANT_OFFSET) >> 29;
if (val < 0) {
return val ^ 0xFFFFFFF8;
}
return val;
}
public set underlineVariantOffset(value: number) {
this._ext &= ~ExtFlags.VARIANT_OFFSET;
this._ext |= (value << 29) & ExtFlags.VARIANT_OFFSET;
}
private _urlId: number = 0;
public get urlId(): number {
return this._urlId;
}
public set urlId(value: number) {
this._urlId = value;
}
class ImageTileInfo {
constructor(
ext: number = 0,
urlId: number = 0,
public imageId = -1,
public tileId = -1
) {
this._ext = ext;
this._urlId = urlId;
}
public clone(): IExtendedAttrsImage {
/**
* Technically we dont need a clone variant of ExtendedAttrsImage,
* as we never clone a cell holding image data.
* Note: Clone is only meant to be used by the InputHandler for
* sticky attributes, which is never the case for image data.
* We still provide a proper clone method to reflect the full ext attr
* state in case there are future use cases for clone.
*/
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
}
public isEmpty(): boolean {
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
public tileId = -1) {
}
}
const EMPTY_ATTRS = new ExtendedAttrsImage();
/**
* ImageStorage - extension of CoreTerminal:
@@ -121,9 +46,12 @@ export class ImageStorage implements IDisposable {
// whether render should do a full clear
private _needsFullClear = false;
// hard limit of stored pixels (fallback limit of 10 MB)
private _pixelLimit: number = 2500000;
private _byteLimit: number = 10000000;
private _workCell: CellData = new CellData();
private _viewportMetrics: { cols: number, rows: number };
public onImageAdded: (() => void) | undefined;
public onImageDeleted: ((storageId: number) => void) | undefined;
constructor(
private _terminal: ITerminalExt,
@@ -132,8 +60,10 @@ export class ImageStorage implements IDisposable {
) {
try {
this.setLimit(this._opts.storageLimit);
} catch (e: any) {
console.error(e.message);
} catch (e: unknown) {
if (e instanceof Error) {
console.error(e.message);
}
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
}
this._viewportMetrics = {
@@ -156,42 +86,55 @@ export class ImageStorage implements IDisposable {
this._renderer.clearAll();
}
/**
* FIXME: below we need to introduce separation of:
* - RAM usage
* - VRAM usage
* - Blob unsage
*/
public getLimit(): number {
return this._pixelLimit * 4 / 1000000;
return this._byteLimit / 1000000;
}
public setLimit(value: number): void {
if (value < 0.5 || value > 1000) {
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1GB');
}
this._pixelLimit = (value / 4 * 1000000) >>> 0;
this._byteLimit = (value * 1000000) >>> 0;
this._evictOldest(0);
}
public getUsage(): number {
return this._getStoredPixels() * 4 / 1000000;
return this._getVRAMUsage() / 1000000;
}
private _getStoredPixels(): number {
let storedPixels = 0;
private _getVRAMUsage(): number {
let bytes = 0;
for (const spec of this._images.values()) {
if (spec.orig) {
storedPixels += spec.orig.width * spec.orig.height;
if (spec.actual && spec.actual !== spec.orig) {
storedPixels += spec.actual.width * spec.actual.height;
}
bytes += spec.src.bytes;
}
return bytes;
}
public getBlobUsage(): number {
let size = 0;
for (const spec of this._images.values()) {
if (spec.data instanceof Blob) {
size += spec.data.size;
}
}
return storedPixels;
return size;
}
private _delImg(id: number): void {
const spec = this._images.get(id);
if (!spec) return;
this._images.delete(id);
// FIXME: really ugly workaround to get bitmaps deallocated :(
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
spec.orig.close();
}
spec.src.close();
spec.data = undefined;
this.onImageDeleted?.(id);
}
/**
@@ -215,37 +158,40 @@ export class ImageStorage implements IDisposable {
}
/**
* Only advance text cursor.
* This is an edge case from empty sixels carrying only a height but no pixels.
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
* Delete an image by its internal storage ID.
* Used by protocols that support explicit deletion (e.g. Kitty a=d).
*/
public advanceCursor(height: number): void {
if (this._opts.sixelScrolling) {
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = CELL_SIZE_DEFAULT;
}
const rows = Math.ceil(height / cellSize.height);
for (let i = 1; i < rows; ++i) {
this._terminal._core._inputHandler.lineFeed();
}
public deleteImage(id: number): void {
const spec = this._images.get(id);
if (spec) {
spec.marker?.dispose();
this._delImg(id);
}
}
/**
* Method to add an image to the storage.
* Method to add an drawable to the storage.
* @param src - The drawable to add.
* @param data - Blob or Uint8Array of the original image data.
* @param metrics - IMetrics of the original image.
* @param opts - Options for addImage:
* - 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): void {
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics, opts: IAddImageOpts): number {
// never allow storage to exceed memory limit
this._evictOldest(img.width * img.height);
this._evictOldest(src.bytes);
// calc rows x cols needed to display the image
let cellSize = this._renderer.cellSize;
if (cellSize.width === -1 || cellSize.height === -1) {
cellSize = CELL_SIZE_DEFAULT;
}
const cols = Math.ceil(img.width / cellSize.width);
const rows = Math.ceil(img.height / cellSize.height);
const prescaleX = opts.prescaleX ?? 1.0;
const prescaleY = opts.prescaleY ?? 1.0;
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
const cols = Math.ceil(src.width * prescaleX / cellSize.width);
const rows = Math.ceil(src.height * prescaleY / cellSize.height);
const imageId = ++this._lastId;
@@ -257,7 +203,7 @@ export class ImageStorage implements IDisposable {
let offset = originX;
let tileCount = 0;
if (!this._opts.sixelScrolling) {
if (!opts.scrolling) {
buffer.x = 0;
buffer.y = 0;
offset = 0;
@@ -265,13 +211,13 @@ export class ImageStorage implements IDisposable {
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(buffer.y + buffer.ybase);
const line = buffer.lines.get(buffer.y + buffer.ybase)!;
for (let col = 0; col < cols; ++col) {
if (offset + col >= termCols) break;
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
this._writeToCell(line, offset + col, imageId, row * cols + col);
tileCount++;
}
if (this._opts.sixelScrolling) {
if (opts.scrolling) {
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
} else {
if (++buffer.y >= termRows) break;
@@ -281,8 +227,12 @@ export class ImageStorage implements IDisposable {
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
// cursor positioning modes
if (this._opts.sixelScrolling) {
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;
@@ -318,17 +268,30 @@ export class ImageStorage implements IDisposable {
// create storage entry
const imgSpec: IImageSpec = {
orig: img,
origCellSize: cellSize,
actual: img,
actualCellSize: { ...cellSize }, // clone needed, since later modified
src,
data,
metrics,
cellSize,
marker: endMarker || undefined,
tileCount,
bufferType: this._terminal.buffer.active.type
bufferType: this._terminal.buffer.active.type,
layer: opts.layer,
zIndex: opts.zIndex,
prescaleX,
prescaleY,
precropX: 0,
precropY: 0,
scaleX: 1.0,
scaleY: 1.0,
offsetX: 0,
offsetY: 0,
blendMode: 'alpha'
};
// finally add the image
this._images.set(imageId, imgSpec);
this.onImageAdded?.();
return imageId;
}
@@ -338,16 +301,30 @@ export class ImageStorage implements IDisposable {
*/
// TODO: Should we move this to the ImageRenderer?
public render(range: { start: number, end: number }): void {
// setup image canvas in case we have none yet, but have images in store
if (!this._renderer.canvas && this._images.size) {
this._renderer.insertLayerToDom();
// safety measure - in case we cannot spawn a canvas at all, just exit
if (!this._renderer.canvas) {
return;
// Determine which layers have images
let hasTopImages = false;
let hasBottomImages = false;
for (const spec of this._images.values()) {
if (spec.layer === 'bottom') {
hasBottomImages = true;
} else {
hasTopImages = true;
}
if (hasTopImages && hasBottomImages) break;
}
// Lazily insert layers that are needed
if (hasTopImages && !this._renderer.hasLayer('top')) {
this._renderer.insertLayerToDom('top');
if (!this._renderer.hasLayer('top')) return;
}
if (hasBottomImages && !this._renderer.hasLayer('bottom')) {
this._renderer.insertLayerToDom('bottom');
}
// rescale if needed
this._renderer.rescaleCanvas();
// exit early if we dont have any images to test for
if (!this._images.size) {
if (!this._fullyCleared) {
@@ -355,12 +332,25 @@ export class ImageStorage implements IDisposable {
this._fullyCleared = true;
this._needsFullClear = false;
}
if (this._renderer.canvas) {
this._renderer.removeLayerFromDom();
if (this._renderer.hasLayer('top')) {
this._renderer.removeLayerFromDom('top');
}
if (this._renderer.hasLayer('bottom')) {
this._renderer.removeLayerFromDom('bottom');
}
return;
}
// Remove layers no longer needed
if (!hasTopImages && this._renderer.hasLayer('top')) {
this._renderer.clearAll('top');
this._renderer.removeLayerFromDom('top');
}
if (!hasBottomImages && this._renderer.hasLayer('bottom')) {
this._renderer.clearAll('bottom');
this._renderer.removeLayerFromDom('bottom');
}
// buffer switches force a full clear
if (this._needsFullClear) {
this._renderer.clearAll();
@@ -375,13 +365,17 @@ export class ImageStorage implements IDisposable {
// clear drawing area
this._renderer.clearLines(start, end);
// walk all cells in viewport and draw tiles found
// Collect draw calls so we can sort by z-index (lower z drawn first).
const drawCalls: { imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number }[] = [];
const placeholderCalls: { col: number, row: number, count: number }[] = [];
// walk all cells in viewport and collect tiles found
for (let row = start; row <= end; ++row) {
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
const line = buffer.lines.get(row + buffer.ydisp);
if (!line) return;
for (let col = 0; col < cols; ++col) {
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
const e = line.getExtended(col)?.payload;
if (e instanceof ImageTileInfo) {
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
@@ -397,28 +391,37 @@ export class ImageStorage implements IDisposable {
* - cell has same image id
* - cell has consecutive tile id
*/
while (
++col < cols
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
&& (e.imageId === imageId)
&& (e.tileId === startTile + count)
) {
while (++col < cols) {
const nextE = line.getExtended(col)?.payload;
if (!(nextE instanceof ImageTileInfo) || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break;
}
count++;
}
col--;
if (imgSpec) {
if (imgSpec.actual) {
this._renderer.draw(imgSpec, startTile, startCol, row, count);
}
drawCalls.push({ imgSpec, tileId: startTile, col: startCol, row, count });
} else if (this._opts.showPlaceholder) {
this._renderer.drawPlaceholder(startCol, row, count);
placeholderCalls.push({ col: startCol, row, count });
}
this._fullyCleared = false;
}
}
}
}
// Sort by z-index so lower z draws first (higher z renders on top)
drawCalls.sort((a, b) => a.imgSpec.zIndex - b.imgSpec.zIndex);
// Draw placeholders first (lowest priority)
for (const call of placeholderCalls) {
this._renderer.drawPlaceholder(call.col, call.row, call.count);
}
// Draw images in z-index order
for (const call of drawCalls) {
this._renderer.draw(call.imgSpec, call.tileId, call.col, call.row, call.count);
}
}
public viewportResize(metrics: { cols: number, rows: number }): void {
@@ -440,9 +443,9 @@ export class ImageStorage implements IDisposable {
const rows = buffer.lines.length;
const oldCol = this._viewportMetrics.cols - 1;
for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
const line = buffer.lines.get(row)!;
const e = line.getExtended(oldCol)?.payload;
if (e instanceof ImageTileInfo) {
const imageId = e.imageId;
if (imageId === undefined || imageId === -1) {
continue;
@@ -452,14 +455,14 @@ export class ImageStorage implements IDisposable {
continue;
}
// found an image tile at oldCol, check if it qualifies for right exapansion
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
const tilesPerRow = Math.ceil(imgSpec.src.width * imgSpec.prescaleX / imgSpec.cellSize.width);
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
continue;
}
// expand only if right side is empty (nothing got wrapped from below)
let hasData = false;
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
if (line.hasContent(rightCol)) {
hasData = true;
break;
}
@@ -471,7 +474,7 @@ export class ImageStorage implements IDisposable {
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
let lastTile = e.tileId;
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
this._writeToCell(line, expandCol, imageId, ++lastTile);
imgSpec.tileCount++;
}
}
@@ -485,34 +488,17 @@ export class ImageStorage implements IDisposable {
*/
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) {
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
const line = buffer.lines.get(y);
if (line) {
const e = line.getExtended(x)?.payload;
if (e instanceof ImageTileInfo && e.imageId && e.imageId !== -1) {
const src = this._images.get(e.imageId)?.src;
if (src?.native instanceof ImageBitmap || src?.native instanceof VideoFrame) {
const canvas = createCanvas(window.document, src.width, src.height);
canvas.getContext('2d')?.drawImage(src.native, 0, 0, src.width, src.height);
return canvas;
}
return orig as HTMLCanvasElement;
}
}
}
/**
* Extract active single tile at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId);
if (spec) {
return this._renderer.extractTile(spec, e.tileId);
}
return src?.native;
}
}
}
@@ -520,15 +506,12 @@ export class ImageStorage implements IDisposable {
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
private _evictOldest(room: number): number {
const used = this._getStoredPixels();
const used = this._getVRAMUsage();
let current = used;
while (this._pixelLimit < current + room && this._images.size) {
while (this._byteLimit < current + room && this._images.size) {
const spec = this._images.get(++this._lowestId);
if (spec && spec.orig) {
current -= spec.orig.width * spec.orig.height;
if (spec.actual && spec.orig !== spec.actual) {
current -= spec.actual.width * spec.actual.height;
}
if (spec) {
current -= spec.src.bytes;
spec.marker?.dispose();
this._delImg(this._lowestId);
}
@@ -536,31 +519,31 @@ export class ImageStorage implements IDisposable {
return used - current;
}
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs[x];
if (old) {
if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
old.imageId = imageId;
old.tileId = tileId;
return;
private _writeToCell(line: IBufferLine, x: number, imageId: number, tileId: number): void {
const workCell = this._workCell;
line.loadCell(x, workCell);
if (this._workCell.hasExtendedAttrs()) {
const old = workCell.extended.payload;
if (old instanceof ImageTileInfo) {
// found an old ExtendedAttrsImage, since we know that
// they are always isolated instances (single cell usage),
// we can re-use it and just update their id entries
const oldSpec = this._images.get(old.imageId);
if (oldSpec) {
// early eviction for in-viewport overwrites
oldSpec.tileCount--;
}
// found a plain ExtendedAttrs instance, clone it to new entry
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
old.imageId = imageId;
old.tileId = tileId;
return;
}
}
// fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
// fall-through: always create new ExtendedAttrs entry
const extattr = workCell.extended.clone();
extattr.payload = new ImageTileInfo(imageId, tileId);
workCell.extended = extattr;
workCell.updateExtended();
line.setCell(x, workCell);
}
private _evictOnAlternate(): void {
@@ -573,18 +556,16 @@ export class ImageStorage implements IDisposable {
// re-count tiles on whole buffer
const buffer = this._terminal._core.buffer;
for (let y = 0; y < this._terminal.rows; ++y) {
const line = buffer.lines.get(y) as IBufferLineExt;
const line = buffer.lines.get(y);
if (!line) {
continue;
}
for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs[x]?.imageId;
if (imgId) {
const spec = this._images.get(imgId);
if (spec) {
spec.tileCount++;
}
const payload = line.getExtended(x)?.payload;
if (payload instanceof ImageTileInfo) {
const spec = this._images.get(payload.imageId);
if (spec) {
spec.tileCount++;
}
}
}
@@ -4,7 +4,8 @@
*/
import { assert } from 'chai';
import { imageType, IMetrics } from './IIPMetrics';
import { imageType } from './Metrics';
import type { IMetrics } from 'Types';
// fix missing nodejs decl
declare const require: (s: string) => any;
@@ -30,7 +31,30 @@ 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 }],
// VP8
['1.webp', { mime: 'image/webp', width: 550, height: 368 }],
['2.webp', { mime: 'image/webp', width: 550, height: 404 }],
['3.webp', { mime: 'image/webp', width: 1280, height: 720 }],
['4.webp', { mime: 'image/webp', width: 1024, height: 772 }],
['5.webp', { mime: 'image/webp', width: 1024, height: 752 }],
// VP8X
['1_webp_a.webp', { mime: 'image/webp', width: 400, height: 301 }],
['2_webp_a.webp', { mime: 'image/webp', width: 386, height: 395 }],
['3_webp_a.webp', { mime: 'image/webp', width: 800, height: 600 }],
['4_webp_a.webp', { mime: 'image/webp', width: 421, height: 163 }],
['5_webp_a.webp', { mime: 'image/webp', width: 300, height: 300 }],
// VP8L
['1_webp_ll.webp', { mime: 'image/webp', width: 400, height: 301 }],
['2_webp_ll.webp', { mime: 'image/webp', width: 386, height: 395 }],
['3_webp_ll.webp', { mime: 'image/webp', width: 800, height: 600 }],
['4_webp_ll.webp', { mime: 'image/webp', width: 421, height: 163 }],
['5_webp_ll.webp', { mime: 'image/webp', width: 300, height: 300 }],
// some AVIF test images
['fox.profile2.12bpc.yuv444.odd-width.odd-height.avif', { mime: 'image/avif', width: 1203, height: 799 }],
['hato.profile0.8bpc.yuv420.avif', { mime: 'image/avif', width: 3082, height: 2048 }],
['kimono.crop.avif', { mime: 'image/avif', width: 722, height: 1024 }],
];
+137
View File
@@ -0,0 +1,137 @@
/**
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IMetrics } from './Types';
export const UNSUPPORTED_TYPE: IMetrics = {
mime: 'unsupported',
width: 0,
height: 0
};
export function imageType(d: Uint8Array): IMetrics {
if (d.length < 32) {
return UNSUPPORTED_TYPE;
}
const d32 = new Uint32Array(d.buffer, d.byteOffset, 8);
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
// + first chunk must be IHDR
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
return {
mime: 'image/png',
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
};
}
// JPEG: FF D8 FF
if (d[0] === 0xFF && d[1] === 0xD8 && d[2] === 0xFF) {
const [width, height] = jpgSize(d);
return { mime: 'image/jpeg', width, height };
}
// GIF: GIF87a or GIF89a
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
return {
mime: 'image/gif',
width: d[7] << 8 | d[6],
height: d[9] << 8 | d[8]
};
}
// QOI: qoif
if (d32[0] === 0x66696F71) {
return {
mime: 'image/qoi',
width: d[4] << 24 | d[5] << 16 | d[6] << 8 | d[7],
height: d[8] << 24 | d[9] << 16 | d[10] << 8 | d[11]
};
}
// WEBP: RIFF | xxxx | WEBP | VP8x
if (d32[0] === 0x46464952 && d32[2] === 0x50424557 && (d32[3] & 0xFFFFFF) === 0x385056) {
switch (d[15]) {
case 0x58: // Extended WebP VP8X --> "X"
return {
mime: 'image/webp',
width: (d[24] | d[25] << 8 | d[26] << 16) + 1,
height: (d[27] | d[28] << 8 | d[29] << 16) + 1
};
case 0x4C: // Lossless WebP VP8L --> "L"
if (d[20] !== 0x2f) return UNSUPPORTED_TYPE;
const dim = d[21] | d[22] << 8 | d[23] << 16 | d[24] << 24;
return {
mime: 'image/webp',
width: (dim & 0x3FFF) + 1,
height: (dim >>> 14 & 0x3FFF) + 1
};
case 0x20: // Lossy WebP VP8 --> " "
if (d[23] !== 0x9d || d[24] !== 0x01 || d[25] !== 0x2a) return UNSUPPORTED_TYPE;
return {
mime: 'image/webp',
width: (d[26] | d[27] << 8) & 0x3FFF,
height: (d[28] | d[29] << 8) & 0x3FFF
};
}
return UNSUPPORTED_TYPE;
}
// AVIF: Box size | ftyp | avif/avis
if (d32[1] === 0x70797466 && (d32[2] === 0x66697661 || d32[2] === 0x73697661)) {
let pos = -1;
// search for ispe box within first 1024 bytes
const limit = Math.min(d.length - 16, 1024);
for (let i = 8; i < limit; i++) {
// scan for ispe
if (d[i] === 0x69 && d[i + 1] === 0x73 && d[i + 2] === 0x70 && d[i + 3] === 0x65) {
pos = i;
break;
}
}
if (pos !== -1) {
// dimensions are in BE at +8 (width) at +12 (height)
const width =
d[pos + 8] << 24 |
d[pos + 9] << 16 |
d[pos + 10] << 8 |
d[pos + 11];
const height =
d[pos + 12] << 24 |
d[pos + 13] << 16 |
d[pos + 14] << 8 |
d[pos + 15];
if (width > 0 && height > 0) {
return { mime: 'image/avif', width, height };
}
}
return UNSUPPORTED_TYPE;
}
return UNSUPPORTED_TYPE;
}
function jpgSize(d: Uint8Array): [number, number] {
const len = d.length;
let i = 4;
let blockLength = d[i] << 8 | d[i + 1];
while (true) {
i += blockLength;
if (i >= len) {
// exhausted without size info
return [0, 0];
}
if (d[i] !== 0xFF) {
return [0, 0];
}
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
if (i + 8 < len) {
return [
d[i + 7] << 8 | d[i + 8],
d[i + 5] << 8 | d[i + 6]
];
}
return [0, 0];
}
i += 2;
blockLength = d[i] << 8 | d[i + 1];
}
}
+68
View File
@@ -0,0 +1,68 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDrawable } from './Types';
export function createCanvas(
localDocument: Document | undefined,
width: number,
height: number,
cssWidth?: number,
cssHeight?: number
): HTMLCanvasElement {
/**
* NOTE: We normally dont care, from which document the canvas
* gets created, so we can fall back to global document,
* if the terminal has no document associated yet.
* This way early image loads before calling .open keep working
* (still discouraged though, as the metrics will be screwed up).
* Only the DOM output canvas should be on the terminal's document,
* which gets explicitly checked in `insertLayerToDom`.
*/
const canvas = (localDocument ?? document).createElement('canvas');
canvas.width = width | 0;
canvas.height = height | 0;
canvas.style.width = `${(cssWidth ?? width) | 0}px`;
canvas.style.height = `${(cssHeight ?? height) | 0}px`;
return canvas;
}
/**
* Drawable is a resource the ImageRenderer knows to draw on the screen.
*/
export class Drawable implements IDrawable {
public readonly width: number;
public readonly height: number;
// @readonly: only for memory tracking
public bytes: number;
constructor(public native: ImageBitmap | VideoFrame | HTMLCanvasElement) {
if (native instanceof VideoFrame) {
this.width = native.displayWidth;
this.height = native.displayHeight;
// NOTE: native.allocationSize() is too expensive
this.bytes = native.codedWidth * native.codedHeight * 4;
} else {
this.width = native.width;
this.height = native.height;
this.bytes = native.width * native.height * 4;
}
}
public close(): void {
if (this.bytes === 0) return;
this.bytes = 0;
try {
if (this.native instanceof HTMLCanvasElement) {
this.native.width = 0;
this.native.height = 0;
} else {
this.native.close?.();
}
} catch {}
this.native = null!;
}
}
+7 -7
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { ImageStorage } from './ImageStorage';
import { SixelImageStorage } from './SixelImageStorage';
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
import { RGBA8888 } from 'sixel/lib/Types';
import { ImageRenderer } from './ImageRenderer';
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
import { createCanvas, Drawable } from './Primitives';
// always free decoder ressources after decoding if it exceeds this limit
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
@@ -26,7 +26,7 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _storage: ImageStorage,
private readonly _storage: SixelImageStorage,
private readonly _coreTerminal: ITerminalExt
) {
DecoderAsync({
@@ -91,19 +91,19 @@ export class SixelHandler implements IDcsHandler, IResetHandler {
const height = this._dec.height;
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
if (!width || ! height) {
if (!width || !height) {
if (height) {
this._storage.advanceCursor(height);
}
return true;
}
const canvas = ImageRenderer.createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
const canvas = createCanvas(undefined, width, height);
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8 as Uint8ClampedArray<ArrayBuffer>, width, height), 0, 0);
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
this._dec.release();
}
this._storage.addImage(canvas);
this._storage.addImage(new Drawable(canvas), undefined, { width, height, mime: 'image/sixel' });
return true;
}
}
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
import { IImageAddonOptions, ITerminalExt, IAddImageOpts, IDrawable, IMetrics } from './Types';
import { ImageRenderer } from './ImageRenderer';
/**
* Sixel-specific image storage controller.
*
* Wraps the shared ImageStorage with sixel protocol semantics:
* - Cursor behavior governed by DECSET 80 (sixelScrolling option)
* - 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,
private readonly _renderer: ImageRenderer,
private readonly _terminal: ITerminalExt
) {}
/**
* Add a sixel image to storage.
* Cursor behavior depends on the sixelScrolling option (DECSET 80).
*/
public addImage(src: IDrawable, data: Blob | undefined, metrics: IMetrics): void {
this._addImageOpts.scrolling = this._opts.sixelScrolling;
this._storage.addImage(src, data, metrics, this._addImageOpts);
}
/**
* Only advance text cursor.
* This is an edge case from empty sixels carrying only a height but no pixels.
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
*/
public advanceCursor(height: number): void {
if (this._opts.sixelScrolling) {
const cellSize = this._renderer.getCellSize() ?? CELL_SIZE_DEFAULT;
const rows = Math.ceil(height / cellSize.height);
for (let i = 1; i < rows; ++i) {
this._terminal._core._inputHandler.lineFeed();
}
}
}
}
+81 -26
View File
@@ -8,8 +8,8 @@ import { IDisposable, IMarker, Terminal } from '@xterm/xterm';
// private imports from base repo we build against
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
import type { AttributeData } from 'common/buffer/AttributeData';
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
import type { IParams, IDcsHandler, IOscHandler, IApcHandler, IEscapeSequenceParser } from 'common/parser/Types';
import type { IInputHandler } from 'common/Types';
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
@@ -22,7 +22,7 @@ export const enum Cell {
}
// export some privates for local usage
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
export { AttributeData, IParams, IDcsHandler, IOscHandler, IApcHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
/**
* Plugin ctor options.
@@ -38,6 +38,8 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
export interface IResetHandler {
@@ -45,6 +47,7 @@ export interface IResetHandler {
reset(): void;
}
/* eslint-disable */
/**
* Stub into private interfaces.
* This should be kept in line with common libs.
@@ -52,19 +55,6 @@ export interface IResetHandler {
* have a somewhat reliable testing against code changes in the core repo.
*/
// overloaded IExtendedAttrs to hold image refs
export interface IExtendedAttrsImage extends IExtendedAttrs {
imageId: number;
tileId: number;
clone(): IExtendedAttrsImage;
}
/* eslint-disable */
export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
_data: Uint32Array;
}
interface IInputHandlerExt extends IInputHandler {
_parser: IEscapeSequenceParser;
_curAttrData: AttributeData;
@@ -75,6 +65,7 @@ interface IInputHandlerExt extends IInputHandler {
};
onRequestReset(handler: () => void): IDisposable;
}
/* eslint-enable */
export interface ICoreTerminalExt extends ITerminal {
_themeService: IThemeService | undefined;
@@ -86,8 +77,6 @@ export interface ICoreTerminalExt extends ITerminal {
export interface ITerminalExt extends Terminal {
_core: ICoreTerminalExt;
}
/* eslint-enable */
/**
* Some storage definitions.
@@ -97,12 +86,78 @@ export interface ICellSize {
height: number;
}
export interface IImageSpec {
orig: HTMLCanvasElement | ImageBitmap | undefined;
origCellSize: ICellSize;
actual: HTMLCanvasElement | ImageBitmap | undefined;
actualCellSize: ICellSize;
marker: IMarker | undefined;
tileCount: number;
bufferType: 'alternate' | 'normal';
export type ImageLayer = 'top' | 'bottom';
export type CursorPos = 'vt340' | 'iip';
export interface IAddImageOpts {
scrolling: boolean;
layer: ImageLayer;
zIndex: number;
cursorPos: CursorPos;
prescaleX?: number;
prescaleY?: number;
}
export interface IDrawable {
native: ImageBitmap | VideoFrame | HTMLCanvasElement;
width: number;
height: number;
close(): void;
readonly bytes: number;
}
export type ImageType = 'unsupported'
| 'image/png'
| 'image/jpeg'
| 'image/gif'
| 'image/qoi'
| 'image/webp'
| 'image/avif'
// sixel
| 'image/sixel'
// rgb|a blobs for kitty
| 'image/rgb'
| 'image/rgba';
export interface IMetrics {
mime: ImageType;
width: number;
height: number;
}
export interface IImageSpec {
/** entries for a later IImageSource type */
/** drawable for screen rendering */
src: IDrawable;
/** image bytes as blob */
data: Blob | undefined;
/** metrics about image like mime and dimensions */
metrics: IMetrics;
/** entries for a later IPlacement type */
/** cell size at time of insert */
cellSize: ICellSize;
/** eviction marker */
marker: IMarker | undefined;
/** used tiles as eviction hint */
tileCount: number;
/** buffer it was placed on */
bufferType: 'alternate' | 'normal';
/** layer it was placed on */
layer: ImageLayer;
zIndex: number;
/** scaling factor on source dimensions, default = 1.0 */
prescaleX: number;
prescaleY: number;
/** crop offset into source, default = 0 (currently unused) */
precropX: number;
precropY: number;
/** output scaling, default = 1.0 (currently unused) */
scaleX: number;
scaleY: number;
/** output offset, default = 0 (currently unused) */
offsetX: number;
offsetY: number;
/** compositing mode on output, default = 'alpha' (currently unused) */
blendMode: 'overwrite' | 'alpha';
}
@@ -0,0 +1,820 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from '@xterm/xterm';
import { IApcHandler, IImageAddonOptions, IResetHandler, ITerminalExt, ImageLayer } from '../Types';
import { ImageRenderer } from '../ImageRenderer';
import { CELL_SIZE_DEFAULT } from '../ImageStorage';
import { KittyImageStorage } from './KittyImageStorage';
import Base64Decoder, { type DecodeStatus } from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
import {
KittyAction,
KittyFormat,
KittyCompression,
IKittyCommand,
IPendingTransmission,
IKittyImageData,
KittyPixelConstants,
parseKittyCommand
} from './KittyGraphicsTypes';
import { createCanvas } from '../Primitives';
const enum Constants {
// Memory limit for base64 decoder (4MB, same as IIPHandler)
DECODER_KEEP_DATA = 4194304,
DECODER_INITIAL_DATA = 4194304, // 4MB
// Local mirror of const enum (esbuild can't inline const enums from external packages)
DECODER_OK = 0,
// Maximum control data size
MAX_CONTROL_DATA_SIZE = 512,
// Semicolon codepoint
SEMICOLON = 0x3B
}
const DECODER_OK = Constants.DECODER_OK as unknown as DecodeStatus.OK;
// Kitty graphics protocol handler with streaming base64 decoding.
export class KittyGraphicsHandler implements IApcHandler, IResetHandler, IDisposable {
private _aborted = false;
private _decodeError = false;
private _activeDecoder: Base64Decoder | null = null;
private readonly _maxEncodedBytes: number;
private readonly _initialEncodedBytes: number;
// Streaming related states
// True while receiving control data (before semicolon).
private _inControlData = true;
// Buffer for control data.
private _controlData = new Uint32Array(Constants.MAX_CONTROL_DATA_SIZE);
private _controlLength = 0;
// Pre-calculated encoded size limit
private _encodedSizeLimit = 0;
private _totalEncodedSize = 0;
// Parsed command. These are the control data before semicolon.
private _parsedCommand: IKittyCommand | null = null;
// Storage related states
private _pendingTransmissions: Map<number, IPendingTransmission> = new Map();
// Tracks the pending key of the most recently started chunked upload.
// Per spec, subsequent chunks only need m= (and optionally q=), without i=.
// When a chunk arrives with no i=, this key is used to find the pending upload.
private _lastPendingKey: number | undefined;
constructor(
private readonly _opts: IImageAddonOptions,
private readonly _renderer: ImageRenderer,
private readonly _kittyStorage: KittyImageStorage,
private readonly _coreTerminal: ITerminalExt
) {
// Convert decoded size limit -> max encoded bytes.
this._maxEncodedBytes = Math.ceil(this._opts.kittySizeLimit * 4 / 3);
// ensure we preallocate more than configured limit while using 4mb initial size.
this._initialEncodedBytes = Math.min(Constants.DECODER_INITIAL_DATA, this._maxEncodedBytes);
}
public reset(): void {
this._cleanupAllPending();
if (this._activeDecoder) {
this._activeDecoder.release();
this._activeDecoder = null;
}
this._kittyStorage.reset();
}
public dispose(): void {
this.reset();
}
private _removePendingEntry(key: number): void {
this._pendingTransmissions.delete(key);
if (this._lastPendingKey === key) {
this._lastPendingKey = undefined;
}
}
private _cleanupAllPending(): void {
for (const pending of this._pendingTransmissions.values()) {
pending.decoder.release();
}
this._pendingTransmissions.clear();
this._lastPendingKey = undefined;
}
public start(): void {
this._aborted = false;
this._decodeError = false;
this._inControlData = true;
this._controlLength = 0;
this._parsedCommand = null;
// Pre-calculate encoded limit once: base64 is 4 bytes encoded → 3 bytes decoded
this._encodedSizeLimit = this._maxEncodedBytes;
this._totalEncodedSize = 0;
this._activeDecoder = null;
}
public put(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
if (!this._inControlData) {
this._streamPayload(data, start, end);
} else {
// Scan for semicolon
let controlEnd = end;
for (let i = start; i < end; i++) {
if (data[i] === Constants.SEMICOLON) {
this._inControlData = false;
controlEnd = i;
break;
}
}
// Copy control data
const copyLength = controlEnd - start;
if (this._controlLength + copyLength > Constants.MAX_CONTROL_DATA_SIZE) {
this._aborted = true;
return;
}
this._controlData.set(data.subarray(start, controlEnd), this._controlLength);
this._controlLength += copyLength;
if (!this._inControlData) {
// Found semicolon - parse control data early for validation
this._parsedCommand = parseKittyCommand(this._parseControlDataString());
// Early validation: i+I conflict
if (this._parsedCommand.id !== undefined && this._parsedCommand.imageNumber !== undefined) {
this._sendResponse(this._parsedCommand.id, 'EINVAL:cannot specify both i and I keys', this._parsedCommand.quiet ?? 0);
this._aborted = true;
return;
}
// Delete action doesn't need payload - skip streaming
if (this._parsedCommand.action === KittyAction.DELETE) {
return;
}
// Stream remaining as payload
const payloadStart = controlEnd + 1;
if (payloadStart < end) {
this._streamPayload(data, payloadStart, end);
}
}
}
}
// Stream payload bytes into the base64 decoder.
private _streamPayload(data: Uint32Array, start: number, end: number): void {
if (this._aborted) return;
// Check size limit (compare encoded bytes against pre-calculated limit)
// Include cumulative size from pending transmission for multi-chunk images.
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
const pendingKey = this._parsedCommand?.id ?? this._lastPendingKey ?? 0;
const pending = this._pendingTransmissions.get(pendingKey);
const previousEncodedSize = pending?.totalEncodedSize ?? 0;
this._totalEncodedSize += end - start;
const cumulativeEncodedSize = previousEncodedSize + this._totalEncodedSize;
if (cumulativeEncodedSize > this._encodedSizeLimit) {
const decoderToRelease = this._activeDecoder ?? pending?.decoder;
if (decoderToRelease) {
decoderToRelease.release();
}
this._activeDecoder = null;
if (pending) {
this._removePendingEntry(pendingKey);
}
this._aborted = true;
return;
}
if (this._decodeError) return;
if (pending?.decoder && !this._activeDecoder) {
this._activeDecoder = pending.decoder;
}
if (!this._activeDecoder) {
this._activeDecoder = new Base64Decoder(Constants.DECODER_KEEP_DATA, this._maxEncodedBytes, this._initialEncodedBytes);
this._activeDecoder.init();
}
if (this._activeDecoder.put(data.subarray(start, end)) !== DECODER_OK) {
this._activeDecoder.release();
this._activeDecoder = null;
this._decodeError = true;
if (pending) {
this._removePendingEntry(pendingKey);
}
}
}
public end(success: boolean): boolean | Promise<boolean> {
if (this._aborted || !success) {
if (this._activeDecoder) {
this._activeDecoder.release();
this._activeDecoder = null;
}
return true;
}
// No semicolon = no payload (delete, capability query)
if (this._inControlData) {
return this._handleNoPayloadCommand();
}
// Use command parsed early in put() - i+I already validated there
const cmd = this._parsedCommand!;
// Delete action was handled by skipping payload - just execute
if (cmd.action === KittyAction.DELETE) {
return this._handleDelete(cmd);
}
// Per spec, subsequent chunks may omit i=, so fall back to _lastPendingKey.
const pendingKey = cmd.id ?? this._lastPendingKey ?? 0;
const isMoreComing = cmd.more === 1;
const pending = this._pendingTransmissions.get(pendingKey);
if (isMoreComing) {
if (this._activeDecoder) {
if (pending) {
pending.totalEncodedSize += this._totalEncodedSize;
pending.decodeError = pending.decodeError || this._decodeError;
} else {
this._pendingTransmissions.set(pendingKey, {
cmd: { ...cmd },
decoder: this._activeDecoder,
totalEncodedSize: this._totalEncodedSize,
decodeError: this._decodeError
});
}
this._lastPendingKey = pendingKey;
this._activeDecoder = null;
}
return true;
}
// Final chunk received — clear the last pending key
if (pending) {
this._lastPendingKey = undefined;
}
let decodeError = this._decodeError;
let finalCmd = cmd;
let decoder = this._activeDecoder;
if (pending) {
finalCmd = pending.cmd;
decoder = pending.decoder;
decodeError = decodeError || pending.decodeError;
this._pendingTransmissions.delete(pendingKey);
}
let imageBytes = new Uint8Array(0);
if (decoder) {
if (decoder.end() !== DECODER_OK) {
decodeError = true;
}
imageBytes = decoder.data8;
}
this._activeDecoder = null;
// Handle command first — handlers create Blob/ImageData from imageBytes,
// which copies the data. Only then is it safe to release the decoder's
// wasm memory that imageBytes points into.
const result = this._handleCommandWithBytesAndCmd(finalCmd, imageBytes, decodeError);
if (decoder) {
decoder.release();
}
return result;
}
// Command handling
private _parseControlDataString(): string {
let str = '';
for (let i = 0; i < this._controlLength; i++) {
str += String.fromCodePoint(this._controlData[i]);
}
return str;
}
private _handleNoPayloadCommand(): boolean | Promise<boolean> {
const cmd = parseKittyCommand(this._parseControlDataString());
// Per spec: specifying both i and I is an error
if (cmd.id !== undefined && cmd.imageNumber !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:cannot specify both i and I keys', cmd.quiet ?? 0);
return true;
}
const action = cmd.action ?? 't';
switch (action) {
case KittyAction.DELETE:
return this._handleDelete(cmd);
case KittyAction.QUERY:
this._sendResponse(cmd.id ?? 0, 'OK', cmd.quiet ?? 0);
return true;
case KittyAction.PLACEMENT:
return this._handlePlacement(cmd);
default:
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
return true;
}
}
private _handleCommandWithBytesAndCmd(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
const action = cmd.action ?? 't';
switch (action) {
case KittyAction.TRANSMIT: {
const result = this._handleTransmit(cmd, bytes, decodeError);
// Only send response when _handleTransmit didn't already respond
// (it handles unsupported transmission medium responses internally)
if ((cmd.transmission ?? 'd') === 'd' && cmd.id !== undefined) {
if (decodeError) {
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
} else if (bytes.length > 0) {
this._sendResponse(cmd.id, 'OK', cmd.quiet ?? 0);
}
}
return result;
}
case KittyAction.TRANSMIT_DISPLAY:
return this._handleTransmitDisplay(cmd, bytes, decodeError);
case KittyAction.QUERY:
return this._handleQuery(cmd, bytes, decodeError);
case KittyAction.PLACEMENT:
// a=p ignores any payload — image data was already transmitted
return this._handlePlacement(cmd);
default:
// TODO: Implement remaining actions when needed:
// - a=f (frame): animation frame operations
// - a=a (animation): animation control
// - a=c (compose): compose images
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported action', cmd.quiet ?? 0);
}
return true;
}
}
private _handlePlacement(cmd: IKittyCommand): boolean | Promise<boolean> {
if (cmd.id === undefined) {
return true;
}
const id = cmd.id;
const image = this._kittyStorage.getImage(id);
if (!image) {
this._sendResponse(id, 'ENOENT:image not found', cmd.quiet ?? 0, cmd.placementId);
return true;
}
const result = this._displayImage(image, cmd);
return result.then(success => {
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0, cmd.placementId);
return true;
});
}
private _handleTransmit(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
// TODO: Support file-based transmission modes (t=f, t=t, t=s)
// Currently only supports direct transmission (t=d, the default).
// - t=f (file): Payload is base64-encoded file path. Terminal reads image from that path.
// - t=t (temp file): Payload is base64-encoded path in temp directory. Terminal reads, deletes.
// - t=s: Payload is base64-encoded POSIX shm name. Terminal reads from shared memory.
// These modes require filesystem/IPC access not available in browsers. For Node.js/Electron:
// 1. Check cmd.transmission (t key) before treating bytes as image data
// 2. For t=f/t/s: decode bytes as UTF-8 string (the path/name), then read file contents
// 3. For t=d: treat bytes as image data (current behavior)
// When implementing, also update _handleQuery to accept these transmission mediums.
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:unsupported transmission medium', cmd.quiet ?? 0);
}
return true;
}
if (decodeError || bytes.length === 0) return true;
this._kittyStorage.storeImage(cmd.id, {
data: new Blob([bytes as BlobPart]),
width: cmd.width ?? 0,
height: cmd.height ?? 0,
format: (cmd.format ?? KittyFormat.RGBA) as 24 | 32 | 100,
compression: cmd.compression ?? ''
});
return true;
}
private _handleTransmitDisplay(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean | Promise<boolean> {
if (decodeError) {
if (cmd.id !== undefined) {
this._sendResponse(cmd.id, 'EINVAL:invalid base64 data', cmd.quiet ?? 0);
}
return true;
}
this._handleTransmit(cmd, bytes, decodeError);
const id = cmd.id ?? this._kittyStorage.lastImageId;
const image = this._kittyStorage.getImage(id);
if (image) {
const result = this._displayImage(image, cmd);
if (cmd.id !== undefined) {
return result.then(success => {
this._sendResponse(id, success ? 'OK' : 'EINVAL:image rendering failed', cmd.quiet ?? 0);
return true;
});
}
return result.then(() => true);
}
return true;
}
private _handleQuery(cmd: IKittyCommand, bytes: Uint8Array, decodeError: boolean): boolean {
const id = cmd.id ?? 0;
const quiet = cmd.quiet ?? 0;
// Per spec: reject unsupported transmission mediums (only t=d is supported atm)
// TODO: When filesystem support is added (Node.js/Electron), update this to accept
// t=f (file), t=t (temp file), and t=s (shared memory) and respond OK for queries.
const transmission = cmd.transmission ?? 'd';
if (transmission !== 'd') {
this._sendResponse(id, 'EINVAL:unsupported transmission medium', quiet);
return true;
}
// Check decode error first (invalid base64)
if (decodeError) {
this._sendResponse(id, 'EINVAL:invalid base64 data', quiet);
return true;
}
// Capability query (no payload) - just respond OK
if (bytes.length === 0) {
this._sendResponse(id, 'OK', quiet);
return true;
}
const format = cmd.format ?? KittyFormat.RGBA;
if (format === KittyFormat.PNG) {
this._sendResponse(id, 'OK', quiet);
} else {
const width = cmd.width ?? 0;
const height = cmd.height ?? 0;
if (!width || !height) {
this._sendResponse(id, 'EINVAL:width and height required for raw pixel data', quiet);
return true;
}
const bytesPerPixel = format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;
if (bytes.length < expectedBytes) {
this._sendResponse(id, `EINVAL:insufficient pixel data`, quiet);
return true;
}
this._sendResponse(id, 'OK', quiet);
}
return true;
}
private _handleDelete(cmd: IKittyCommand): boolean {
// Per spec: default delete selector is 'a' (delete all visible placements)
const selector = cmd.deleteSelector ?? 'a';
// TODO: Distinguish lowercase (delete placements only) from uppercase
// (delete placements + free stored image data). Currently both variants
// free everything since we don't separate stored data from placements.
switch (selector) {
case 'a':
case 'A':
this._cleanupAllPending();
this._kittyStorage.deleteAll();
break;
case 'i':
case 'I':
// TODO: When placement id tracking is implemented (see TODO in
// KittyImageStorage), d=i with p=<pid> should delete only that
// specific placement, while d=i without p should delete all
// placements for the image.
if (cmd.id !== undefined) {
const pending = this._pendingTransmissions.get(cmd.id);
if (pending) {
pending.decoder.release();
}
this._removePendingEntry(cmd.id);
this._kittyStorage.deleteById(cmd.id);
}
break;
default:
// Unsupported selectors (c, n, p, q, r, x, y, z, f) — ignore for now
break;
}
return true;
}
private _sendResponse(id: number, message: string, quiet: number, placementId?: number): void {
const isOk = message === 'OK';
if (isOk && quiet >= 1) return;
if (!isOk && quiet >= 2) return;
const pPart = placementId ? `,p=${placementId}` : '';
const response = `\x1b_Gi=${id}${pPart};${message}\x1b\\`;
this._coreTerminal._core.coreService.triggerDataEvent(response);
}
// Image display
private _displayImage(image: IKittyImageData, cmd: IKittyCommand): Promise<boolean> {
return this._decodeAndDisplay(image, cmd)
.then(() => true)
.catch(() => false);
}
private async _decodeAndDisplay(image: IKittyImageData, cmd: IKittyCommand): Promise<void> {
let bitmap: ImageBitmap | undefined = await this._createBitmap(image);
try {
const cropX = Math.max(0, cmd.x ?? 0);
const cropY = Math.max(0, cmd.y ?? 0);
const cropW = cmd.sourceWidth || (bitmap.width - cropX);
const cropH = cmd.sourceHeight || (bitmap.height - cropY);
const maxCropW = Math.max(0, bitmap.width - cropX);
const maxCropH = Math.max(0, bitmap.height - cropY);
const finalCropW = Math.max(0, Math.min(cropW, maxCropW));
const finalCropH = Math.max(0, Math.min(cropH, maxCropH));
if (finalCropW === 0 || finalCropH === 0) {
throw new Error('invalid source rectangle');
}
if (cropX !== 0 || cropY !== 0 || finalCropW !== bitmap.width || finalCropH !== bitmap.height) {
const cropped = await createImageBitmap(bitmap, cropX, cropY, finalCropW, finalCropH);
bitmap.close();
bitmap = cropped;
}
const cw = this._renderer.dimensions?.device.cell.width || CELL_SIZE_DEFAULT.width;
const ch = this._renderer.dimensions?.device.cell.height || CELL_SIZE_DEFAULT.height;
// Per spec: c/r default to image's natural cell dimensions.
// If only one of c/r is specified, compute the other from image aspect ratio.
let imgCols: number;
let imgRows: number;
if (cmd.columns !== undefined && cmd.rows !== undefined) {
imgCols = cmd.columns;
imgRows = cmd.rows;
} else if (cmd.columns !== undefined) {
imgCols = cmd.columns;
imgRows = Math.max(1, Math.ceil((bitmap.height / bitmap.width) * (imgCols * cw) / ch));
} else if (cmd.rows !== undefined) {
imgRows = cmd.rows;
imgCols = Math.max(1, Math.ceil((bitmap.width / bitmap.height) * (imgRows * ch) / cw));
} else {
imgCols = Math.ceil(bitmap.width / cw);
imgRows = Math.ceil(bitmap.height / ch);
}
let w = bitmap.width;
let h = bitmap.height;
// Scale bitmap to fit placement rectangle when c/r are specified
if (cmd.columns !== undefined || cmd.rows !== undefined) {
w = Math.round(imgCols * cw);
h = Math.round(imgRows * ch);
}
if (w * h > this._opts.pixelLimit) {
throw new Error('image exceeds pixel limit');
}
// Save cursor position before addImage modifies it
const buffer = this._coreTerminal._core.buffer;
const savedX = buffer.x;
const savedY = buffer.y;
const savedYbase = buffer.ybase;
// Determine layer based on z-index: negative = behind text, 0+ = on top.
// When z<0 we always use the bottom layer even without allowTransparency —
// the image will simply be hidden behind the opaque text background, which
// is the correct behavior (client asked for "behind text").
const wantsBottom = cmd.zIndex !== undefined && cmd.zIndex < 0;
const layer: ImageLayer = wantsBottom ? 'bottom' : 'top';
if (w !== bitmap.width || h !== bitmap.height) {
const scaled = await createImageBitmap(bitmap, { resizeWidth: w, resizeHeight: h });
bitmap.close();
bitmap = scaled;
}
// Per spec: X/Y are pixel offsets within the first cell, so clamp to cell dimensions
const xOffset = Math.min(Math.max(0, cmd.xOffset ?? 0), cw - 1);
const yOffset = Math.min(Math.max(0, cmd.yOffset ?? 0), ch - 1);
if (xOffset !== 0 || yOffset !== 0) {
// Per spec: X/Y is not added to c/r area. When c/r are explicit, the
// total placement area remains c*cw × r*ch pixels and the offset image
// is clipped to fit. When c/r are unset, the padded canvas determines
// the natural cell dimensions.
const canvasW = (cmd.columns !== undefined) ? Math.round(imgCols * cw) : bitmap.width + xOffset;
const canvasH = (cmd.rows !== undefined) ? Math.round(imgRows * ch) : bitmap.height + yOffset;
const offsetCanvas = createCanvas(window.document, canvasW, canvasH);
const offsetCtx = offsetCanvas.getContext('2d');
if (!offsetCtx) {
throw new Error('Failed to create offset canvas context');
}
offsetCtx.drawImage(bitmap, xOffset, yOffset);
const offsetBitmap = await createImageBitmap(offsetCanvas);
offsetCanvas.width = offsetCanvas.height = 0;
bitmap.close();
bitmap = offsetBitmap;
w = bitmap.width;
h = bitmap.height;
if (w * h > this._opts.pixelLimit) {
throw new Error('image exceeds pixel limit');
}
if (cmd.columns === undefined) {
imgCols = Math.ceil(bitmap.width / cw);
}
if (cmd.rows === undefined) {
imgRows = Math.ceil(bitmap.height / ch);
}
}
const zIndex = cmd.zIndex ?? 0;
this._kittyStorage.addImage(image.id, bitmap, true, layer, zIndex);
bitmap = undefined; // ownership transferred to storage
// Kitty cursor movement
// Per spec: cursor placed at first column after last image column,
// on the last row of the image. C=1 means don't move cursor.
if (cmd.cursorMovement === 1) {
// C=1: restore cursor to position before image was placed
const scrolled = buffer.ybase - savedYbase;
buffer.x = savedX;
// Can't restore cursor to scrollback?
buffer.y = Math.max(savedY - scrolled, 0);
} else {
// Default (C=0): advance cursor horizontally past the image
// addImage already positioned cursor on the last row via lineFeeds
buffer.x = Math.min(savedX + imgCols, this._coreTerminal.cols);
}
} catch (e) {
bitmap?.close();
throw e;
}
}
// Create ImageBitmap from already-decoded image data.
private async _createBitmap(image: IKittyImageData): Promise<ImageBitmap> {
let bytes: Uint8Array = new Uint8Array(await image.data.arrayBuffer());
if (image.compression === KittyCompression.ZLIB) {
bytes = await this._decompressZlib(bytes);
}
if (image.format === KittyFormat.PNG) {
const blob = new Blob([bytes as BlobPart], { type: 'image/png' });
if (!window.createImageBitmap) {
const url = URL.createObjectURL(blob);
const img = new Image();
return new Promise<ImageBitmap>((resolve, reject) => {
img.addEventListener('load', () => {
URL.revokeObjectURL(url);
const canvas = createCanvas(window.document, img.width, img.height);
canvas.getContext('2d')?.drawImage(img, 0, 0);
createImageBitmap(canvas).then(resolve).catch(reject);
});
img.addEventListener('error', () => {
URL.revokeObjectURL(url);
reject(new Error('Failed to load image'));
});
img.src = url;
});
}
return createImageBitmap(blob);
}
// Raw pixel data
const width = image.width;
const height = image.height;
if (!width || !height) {
throw new Error('Width and height required for raw pixel data');
}
const bytesPerPixel = image.format === KittyFormat.RGBA ? KittyPixelConstants.BYTES_PER_PIXEL_RGBA : KittyPixelConstants.BYTES_PER_PIXEL_RGB;
const expectedBytes = width * height * bytesPerPixel;
if (bytes.length < expectedBytes) {
throw new Error('Insufficient pixel data');
}
const pixelCount = width * height;
if (image.format === KittyFormat.RGBA) {
// RGBA: use bytes directly — no copy needed
return createImageBitmap(new ImageData(new Uint8ClampedArray(bytes.buffer as ArrayBuffer, bytes.byteOffset, pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA), width, height));
}
// RGB→RGBA: interleave alpha using uint32 block processing (4 pixels per iteration).
// 3 uint32 reads + 4 uint32 writes per 4 pixels vs 28 byte reads/writes — ~6x faster.
// Assumes little-endian (all modern browsers/Node.js).
const data = new Uint8ClampedArray(pixelCount * KittyPixelConstants.BYTES_PER_PIXEL_RGBA);
const src32 = new Uint32Array(bytes.buffer, bytes.byteOffset, Math.floor(bytes.byteLength / 4));
const dst32 = new Uint32Array(data.buffer);
const alignedPixels = pixelCount & ~3; // round down to multiple of 4
let srcOffset = 0;
let dstOffset = 0;
for (let i = 0; i < alignedPixels; i += 4) {
const b0 = src32[srcOffset++];
const b1 = src32[srcOffset++];
const b2 = src32[srcOffset++];
// Little-endian: pixel bytes are [R,G,B] → uint32 ABGR layout
dst32[dstOffset++] = 0xFF000000 | b0;
dst32[dstOffset++] = 0xFF000000 | (b0 >>> 24) | (b1 << 8);
dst32[dstOffset++] = 0xFF000000 | (b1 >>> 16) | (b2 << 16);
dst32[dstOffset++] = 0xFF000000 | (b2 >>> 8);
}
// Handle remaining 1–3 pixels
let srcByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGB;
let dstByte = alignedPixels * KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
for (let i = alignedPixels; i < pixelCount; i++) {
data[dstByte] = bytes[srcByte];
data[dstByte + 1] = bytes[srcByte + 1];
data[dstByte + 2] = bytes[srcByte + 2];
data[dstByte + 3] = KittyPixelConstants.ALPHA_OPAQUE;
srcByte += KittyPixelConstants.BYTES_PER_PIXEL_RGB;
dstByte += KittyPixelConstants.BYTES_PER_PIXEL_RGBA;
}
return createImageBitmap(new ImageData(data, width, height));
}
private async _decompressZlib(compressed: Uint8Array): Promise<Uint8Array> {
try {
return await this._decompress(compressed, 'deflate');
} catch {
return await this._decompress(compressed, 'deflate-raw');
}
}
private async _decompress(compressed: Uint8Array, format: 'deflate' | 'deflate-raw'): Promise<Uint8Array> {
const ds = new DecompressionStream(format);
const writer = ds.writable.getWriter();
writer.write(compressed as BufferSource);
writer.close();
const chunks: Uint8Array[] = [];
const reader = ds.readable.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const result = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
public get images(): ReadonlyMap<number, IKittyImageData> {
return this._kittyStorage.images;
}
public get _kittyIdToStorageId(): ReadonlyMap<number, number> {
return this._kittyStorage.kittyIdToStorageId;
}
public get pendingTransmissions(): ReadonlyMap<number, IPendingTransmission> {
return this._pendingTransmissions;
}
}
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { parseKittyCommand, KittyAction, KittyFormat } from './KittyGraphicsTypes';
describe('KittyGraphicsTypes', () => {
describe('parseKittyCommand', () => {
it('should parse control data with action and format', () => {
const cmd = parseKittyCommand('a=T,f=100');
assert.strictEqual(cmd.action, 'T');
assert.strictEqual(cmd.format, 100);
});
it('should parse control data with all options', () => {
const cmd = parseKittyCommand('a=t,f=32,i=5,s=10,v=20,c=3,r=2,m=1,q=2');
assert.strictEqual(cmd.action, 't');
assert.strictEqual(cmd.format, 32);
assert.strictEqual(cmd.id, 5);
assert.strictEqual(cmd.width, 10);
assert.strictEqual(cmd.height, 20);
assert.strictEqual(cmd.columns, 3);
assert.strictEqual(cmd.rows, 2);
assert.strictEqual(cmd.more, 1);
assert.strictEqual(cmd.quiet, 2);
});
it('should handle empty control data', () => {
const cmd = parseKittyCommand('');
assert.strictEqual(cmd.action, undefined);
assert.strictEqual(cmd.format, undefined);
});
it('should parse transmit action', () => {
const cmd = parseKittyCommand('a=t,f=100');
assert.strictEqual(cmd.action, KittyAction.TRANSMIT);
assert.strictEqual(cmd.format, KittyFormat.PNG);
});
it('should parse delete action', () => {
const cmd = parseKittyCommand('a=d,i=5');
assert.strictEqual(cmd.action, KittyAction.DELETE);
assert.strictEqual(cmd.id, 5);
});
it('should parse empty action as empty string', () => {
const cmd = parseKittyCommand('a=,f=100');
assert.strictEqual(cmd.action, '');
assert.strictEqual(cmd.format, 100);
});
it('should leave action undefined when key is not present', () => {
const cmd = parseKittyCommand('f=100,i=5');
assert.strictEqual(cmd.action, undefined);
assert.strictEqual(cmd.format, 100);
assert.strictEqual(cmd.id, 5);
});
it('should parse compression key', () => {
const cmd = parseKittyCommand('a=t,f=32,o=z');
assert.strictEqual(cmd.action, 't');
assert.strictEqual(cmd.format, 32);
assert.strictEqual(cmd.compression, 'z');
});
it('should parse cursor movement key', () => {
const cmd = parseKittyCommand('a=T,f=100,C=1');
assert.strictEqual(cmd.cursorMovement, 1);
});
it('should parse cursor movement key C=0', () => {
const cmd = parseKittyCommand('a=T,f=100,C=0');
assert.strictEqual(cmd.cursorMovement, 0);
});
it('should parse x and y offset', () => {
const cmd = parseKittyCommand('a=T,x=10,y=20');
assert.strictEqual(cmd.x, 10);
assert.strictEqual(cmd.y, 20);
});
it('should handle keys without values', () => {
const cmd = parseKittyCommand('a=t,f=,i=5');
assert.strictEqual(cmd.action, 't');
assert.ok(isNaN(cmd.format!));
assert.strictEqual(cmd.id, 5);
});
it('should parse z-index key with positive value', () => {
const cmd = parseKittyCommand('a=T,f=100,z=10');
assert.strictEqual(cmd.zIndex, 10);
});
it('should parse z-index key with zero', () => {
const cmd = parseKittyCommand('a=T,f=100,z=0');
assert.strictEqual(cmd.zIndex, 0);
});
it('should parse z-index key with negative value', () => {
const cmd = parseKittyCommand('a=T,f=100,z=-1');
assert.strictEqual(cmd.zIndex, -1);
});
it('should leave zIndex undefined when not specified', () => {
const cmd = parseKittyCommand('a=T,f=100');
assert.strictEqual(cmd.zIndex, undefined);
});
it('should parse delete selector key', () => {
const cmd = parseKittyCommand('a=d,d=i,i=5');
assert.strictEqual(cmd.action, 'd');
assert.strictEqual(cmd.deleteSelector, 'i');
assert.strictEqual(cmd.id, 5);
});
it('should parse uppercase delete selector', () => {
const cmd = parseKittyCommand('a=d,d=A');
assert.strictEqual(cmd.deleteSelector, 'A');
});
it('should parse delete selector d=a (all)', () => {
const cmd = parseKittyCommand('a=d,d=a');
assert.strictEqual(cmd.deleteSelector, 'a');
});
it('should leave deleteSelector undefined when not specified', () => {
const cmd = parseKittyCommand('a=d,i=5');
assert.strictEqual(cmd.deleteSelector, undefined);
});
it('should parse placement id key', () => {
const cmd = parseKittyCommand('a=d,d=i,i=5,p=3');
assert.strictEqual(cmd.placementId, 3);
assert.strictEqual(cmd.deleteSelector, 'i');
assert.strictEqual(cmd.id, 5);
});
it('should leave placementId undefined when not specified', () => {
const cmd = parseKittyCommand('a=d,d=i,i=5');
assert.strictEqual(cmd.placementId, undefined);
});
it('should parse image number key', () => {
const cmd = parseKittyCommand('a=t,f=100,I=42');
assert.strictEqual(cmd.imageNumber, 42);
});
});
});
@@ -0,0 +1,195 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*
* Kitty graphics protocol types, constants, and parsing utilities.
*/
import type Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
// Kitty graphics protocol action types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'a'.
export const enum KittyAction {
TRANSMIT = 't',
TRANSMIT_DISPLAY = 'T',
QUERY = 'q',
PLACEMENT = 'p',
DELETE = 'd'
}
// Kitty graphics protocol format types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
export const enum KittyFormat {
RGB = 24,
RGBA = 32,
PNG = 100
}
// Kitty graphics protocol compression types.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference under key 'o'.
export const enum KittyCompression {
NONE = '',
ZLIB = 'z'
}
// Kitty graphics protocol control data keys.
// See: https://sw.kovidgoyal.net/kitty/graphics-protocol/#control-data-reference
export const enum KittyKey {
// Action to perform (t=transmit, T=transmit+display, q=query, p=placement, d=delete)
ACTION = 'a',
// Image format (24=RGB, 32=RGBA, 100=PNG)
FORMAT = 'f',
// Image ID for referencing stored images
ID = 'i',
// Image number (alternative to ID, terminal assigns ID)
IMAGE_NUMBER = 'I',
// Source image width in pixels
WIDTH = 's',
// Source image height in pixels
HEIGHT = 'v',
// The left edge (in pixels) of the image area to display
X_OFFSET = 'x',
// The top edge (in pixels) of the image area to display
Y_OFFSET = 'y',
// Width (in pixels) of the source rectangle to display
SOURCE_WIDTH = 'w',
// Height (in pixels) of the source rectangle to display
SOURCE_HEIGHT = 'h',
// Horizontal offset (in pixels) within the first cell
X_PLACEMENT_OFFSET = 'X',
// Vertical offset (in pixels) within the first cell
Y_PLACEMENT_OFFSET = 'Y',
// Number of terminal columns to display the image over
COLUMNS = 'c',
// Number of terminal rows to display the image over
ROWS = 'r',
// More data flag (1=more chunks coming, 0=final chunk)
MORE = 'm',
// Compression type (z=zlib). This is essential for chunking larger images.
COMPRESSION = 'o',
// Quiet mode (1=suppress OK responses, 2=suppress error responses)
QUIET = 'q',
// Cursor movement policy (0=move cursor after image, 1=don't move cursor)
CURSOR_MOVEMENT = 'C',
// Z-index for image layering (negative = behind text, 0+ = on top)
Z_INDEX = 'z',
// Transmission medium (d=direct, f=file, t=temp file, s=shared memory)
TRANSMISSION = 't',
// Delete selector (a/A=all, i/I=by id, c/C=at cursor, etc.) — only used when a=d
DELETE_SELECTOR = 'd',
// Placement ID for targeting specific placements
PLACEMENT_ID = 'p'
}
// Pixel format constants
export const enum KittyPixelConstants {
BYTES_PER_PIXEL_RGB = 3,
BYTES_PER_PIXEL_RGBA = 4,
ALPHA_OPAQUE = 255
}
// Parsed Kitty graphics command.
export interface IKittyCommand {
action?: string;
format?: number;
id?: number;
imageNumber?: number;
width?: number;
height?: number;
x?: number;
y?: number;
sourceWidth?: number;
sourceHeight?: number;
xOffset?: number;
yOffset?: number;
columns?: number;
rows?: number;
more?: number;
quiet?: number;
cursorMovement?: number;
zIndex?: number;
transmission?: string;
deleteSelector?: string;
placementId?: number;
compression?: string;
payload?: string;
}
// Pending chunked transmission state.
// Stores metadata from the first chunk while accumulating decoded payload data.
export interface IPendingTransmission {
// The parsed command from the first chunk (contains action, format, dimensions, etc.)
cmd: IKittyCommand;
// Decoder used across chunked payloads
decoder: Base64Decoder;
// Total encoded (base64) bytes received across all chunks - for size limit enforcement
totalEncodedSize: number;
// Whether any chunk has failed to decode
decodeError: boolean;
}
// Stored Kitty image data.
export interface IKittyImageData {
id: number;
// Decoded image data stored as Blob (off JS heap) to avoid 2GB heap limit
data: Blob;
width: number;
height: number;
format: 24 | 32 | 100;
compression?: string;
}
// Parses Kitty graphics control data into a command object.
export function parseKittyCommand(data: string): IKittyCommand {
const cmd: IKittyCommand = {};
const parts = data.split(',');
for (const part of parts) {
const eqIdx = part.indexOf('=');
if (eqIdx === -1) continue;
const key = part.substring(0, eqIdx);
const value = part.substring(eqIdx + 1);
// Handle string keys first
if (key === KittyKey.ACTION) {
cmd.action = value;
continue;
}
if (key === KittyKey.COMPRESSION) {
cmd.compression = value;
continue;
}
if (key === KittyKey.TRANSMISSION) {
cmd.transmission = value;
continue;
}
if (key === KittyKey.DELETE_SELECTOR) {
cmd.deleteSelector = value;
continue;
}
const numValue = parseInt(value, 10);
switch (key) {
case KittyKey.FORMAT: cmd.format = numValue; break;
case KittyKey.ID: cmd.id = numValue; break;
case KittyKey.IMAGE_NUMBER: cmd.imageNumber = numValue; break;
case KittyKey.WIDTH: cmd.width = numValue; break;
case KittyKey.HEIGHT: cmd.height = numValue; break;
case KittyKey.X_OFFSET: cmd.x = numValue; break;
case KittyKey.Y_OFFSET: cmd.y = numValue; break;
case KittyKey.SOURCE_WIDTH: cmd.sourceWidth = numValue; break;
case KittyKey.SOURCE_HEIGHT: cmd.sourceHeight = numValue; break;
case KittyKey.X_PLACEMENT_OFFSET: cmd.xOffset = numValue; break;
case KittyKey.Y_PLACEMENT_OFFSET: cmd.yOffset = numValue; break;
case KittyKey.COLUMNS: cmd.columns = numValue; break;
case KittyKey.ROWS: cmd.rows = numValue; break;
case KittyKey.MORE: cmd.more = numValue; break;
case KittyKey.QUIET: cmd.quiet = numValue; break;
case KittyKey.CURSOR_MOVEMENT: cmd.cursorMovement = numValue; break;
case KittyKey.Z_INDEX: cmd.zIndex = numValue; break;
case KittyKey.PLACEMENT_ID: cmd.placementId = numValue; break;
}
}
return cmd;
}
@@ -0,0 +1,158 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from '@xterm/xterm';
import { ImageStorage } from '../ImageStorage';
import { ImageLayer, IAddImageOpts } from '../Types';
import { IKittyImageData } from './KittyGraphicsTypes';
import { Drawable } from '../Primitives';
import { UNSUPPORTED_TYPE } from '../Metrics';
// Kitty-specific image storage controller.
//
// Wraps shared ImageStorage with kitty protocol semantics:
// - tracks transmitted image payloads by kitty image id
// - tracks kitty image id -> shared ImageStorage id mapping for displayed images
// - mirrors shared-storage evictions into kitty maps
// - applies protocol-level undisplayed-image eviction policy
export class KittyImageStorage implements IDisposable {
private static readonly _maxStoredImages = 256;
private _nextImageId = 1;
private readonly _images: Map<number, IKittyImageData> = new Map();
// TODO: Support multiple placements per image. The kitty spec identifies
// placements by an (image id, placement id) pair — same i + different p
// values should coexist, and same i + same p should replace the prior
// placement. Currently we track only one storage entry per kitty image id,
// so multiple placements of the same image overwrite each other. Fixing
// this requires changing these maps to Map<number, Map<number, number>>
// (kittyId → placementId → storageId) and updating addImage/deleteById
// accordingly. The underlying shared ImageStorage would also need to
// support multiple entries per logical image.
private readonly _kittyIdToStorageId: Map<number, number> = new Map();
private readonly _storageIdToKittyId: Map<number, number> = new Map();
private readonly _previousOnImageDeleted: ((storageId: number) => void) | undefined;
private readonly _wrappedOnImageDeleted: (storageId: number) => void;
private readonly _handleStorageImageDeleted = (storageId: number): void => {
const kittyId = this._storageIdToKittyId.get(storageId);
if (kittyId !== undefined) {
this._kittyIdToStorageId.delete(kittyId);
this._storageIdToKittyId.delete(storageId);
this._images.delete(kittyId);
}
};
private _addImageOpts: IAddImageOpts = { scrolling: true, layer: 'top', zIndex: 0, cursorPos: 'iip' };
constructor(
private readonly _storage: ImageStorage
) {
this._previousOnImageDeleted = this._storage.onImageDeleted;
this._wrappedOnImageDeleted = (storageId: number) => {
this._previousOnImageDeleted?.(storageId);
this._handleStorageImageDeleted(storageId);
};
this._storage.onImageDeleted = this._wrappedOnImageDeleted;
}
public reset(): void {
this._nextImageId = 1;
this._images.clear();
this._kittyIdToStorageId.clear();
this._storageIdToKittyId.clear();
}
public dispose(): void {
this.reset();
if (this._storage.onImageDeleted === this._wrappedOnImageDeleted) {
this._storage.onImageDeleted = this._previousOnImageDeleted;
}
}
public storeImage(id: number | undefined, imageData: Omit<IKittyImageData, 'id'>): number {
const imageId = id ?? this._nextImageId++;
const oldStorageId = this._kittyIdToStorageId.get(imageId);
if (oldStorageId !== undefined) {
this._storage.deleteImage(oldStorageId);
this._kittyIdToStorageId.delete(imageId);
this._storageIdToKittyId.delete(oldStorageId);
}
if (!this._images.has(imageId) && this._images.size >= KittyImageStorage._maxStoredImages) {
this._evictUndisplayedImages();
}
this._images.set(imageId, {
...imageData,
id: imageId
});
return imageId;
}
public addImage(kittyId: number, image: HTMLCanvasElement | ImageBitmap, scrolling: boolean, layer: ImageLayer, zIndex: number): void {
// Clean up stale reverse-mapping from a previous placement of the same
// kitty image. The old shared-storage entry is kept (it may still be
// visible on screen) but its reverse mapping is removed so that eviction
// of the old entry won't incorrectly delete the kitty image data.
const oldStorageId = this._kittyIdToStorageId.get(kittyId);
if (oldStorageId !== undefined) {
this._storageIdToKittyId.delete(oldStorageId);
}
this._addImageOpts.scrolling = scrolling;
this._addImageOpts.layer = layer;
this._addImageOpts.zIndex = zIndex;
// FIXME: store image data in blob, fill metrics correctly
const storageId = this._storage.addImage(new Drawable(image), undefined, UNSUPPORTED_TYPE, this._addImageOpts);
this._kittyIdToStorageId.set(kittyId, storageId);
this._storageIdToKittyId.set(storageId, kittyId);
}
public getImage(kittyId: number): IKittyImageData | undefined {
return this._images.get(kittyId);
}
public deleteById(kittyId: number): void {
this._images.delete(kittyId);
const storageId = this._kittyIdToStorageId.get(kittyId);
if (storageId !== undefined) {
this._storage.deleteImage(storageId);
this._kittyIdToStorageId.delete(kittyId);
this._storageIdToKittyId.delete(storageId);
}
}
public deleteAll(): void {
this._images.clear();
for (const storageId of this._kittyIdToStorageId.values()) {
this._storage.deleteImage(storageId);
}
this._kittyIdToStorageId.clear();
this._storageIdToKittyId.clear();
}
public get images(): ReadonlyMap<number, IKittyImageData> {
return this._images;
}
public get kittyIdToStorageId(): ReadonlyMap<number, number> {
return this._kittyIdToStorageId;
}
public get lastImageId(): number {
return this._nextImageId - 1;
}
private _evictUndisplayedImages(): void {
for (const [kittyId] of this._images) {
if (this._images.size <= KittyImageStorage._maxStoredImages / 2) {
break;
}
if (!this._kittyIdToStorageId.has(kittyId)) {
this._images.delete(kittyId);
}
}
}
}
+3 -5
View File
@@ -11,12 +11,11 @@
"types": [
"../../../node_modules/@types/mocha"
],
"baseUrl": ".",
"paths": {
"browser/*": [ "../../../src/browser/*" ],
"common/*": [ "../../../src/common/*" ],
"vs/*": [ "../../../src/vs/*" ],
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ]
"@xterm/addon-image": [ "../typings/addon-image.d.ts" ],
"*": [ "./*" ]
}
},
"include": [
@@ -25,7 +24,6 @@
],
"references": [
{ "path": "../../../src/browser" },
{ "path": "../../../src/common" },
{ "path": "../../../src/vs" }
{ "path": "../../../src/common" }
]
}
+193 -22
View File
@@ -8,6 +8,7 @@ import { readFileSync } from 'fs';
import { FINALIZER, introducer, sixelEncode } from 'sixel';
import { ITestContext, createTestContext, openTerminal, pollFor, timeout } from '../../../test/playwright/TestUtils';
import { deepStrictEqual, ok, strictEqual } from 'assert';
import QoiEncoder from 'xterm-wasm-parts/lib/qoi/QoiEncoder.wasm';
/**
* Plugin ctor options.
@@ -23,6 +24,8 @@ export interface IImageAddonOptions {
sixelSizeLimit: number;
iipSupport: boolean;
iipSizeLimit: number;
kittySupport: boolean;
kittySizeLimit: number;
}
// eslint-disable-next-line
@@ -72,6 +75,10 @@ const TESTDATA_IIP: [string, [number, number]][] = [
[readFileSync('./addons/addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
[readFileSync('./addons/addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
];
const PALETTE_PNG_BASE64 = readFileSync('./addons/addon-image/fixture/palette.png').toString('base64');
const qoiEnc = new QoiEncoder(1024*1024);
const qoiData = qoiEnc.encode(TESTDATA.bytes, 640, 80);
const PALETTE_QOI_BASE64 = Buffer.from(qoiData).toString('base64');
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
@@ -83,11 +90,6 @@ test.afterAll(async () => await ctx.page.close());
test.describe('ImageAddon', () => {
test.beforeEach(async ({}, testInfo) => {
// DEBT: This test never worked on webkit
if (ctx.browser.browserType().name() === 'webkit') {
testInfo.skip();
return;
}
await ctx.page.evaluate(`
window.term.reset()
window.imageAddon?.dispose();
@@ -130,11 +132,13 @@ test.describe('ImageAddon', () => {
sixelSupport: true,
sixelScrolling: true,
sixelPaletteLimit: 512, // set to 512 to get example image working
sixelSizeLimit: 25000000,
sixelSizeLimit: 33554432,
storageLimit: 128,
showPlaceholder: true,
iipSupport: true,
iipSizeLimit: 20000000
iipSizeLimit: 33554432,
kittySupport: true,
kittySizeLimit: 33554432
};
deepStrictEqual(await ctx.page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
});
@@ -149,7 +153,9 @@ test.describe('ImageAddon', () => {
storageLimit: 10,
showPlaceholder: false,
iipSupport: false,
iipSizeLimit: 1000
iipSizeLimit: 1000,
kittySupport: false,
kittySizeLimit: 1000
};
await ctx.page.evaluate(opts => {
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
@@ -190,6 +196,18 @@ test.describe('ImageAddon', () => {
});
test.describe('image lifecycle & eviction', () => {
test('onImageAdded fires for each image', async () => {
await ctx.page.evaluate(`
window._imageAddedCount = 0;
window.imageAddon.onImageAdded(() => { window._imageAddedCount++; });
`);
await ctx.proxy.write(SIXEL_SEQ_0);
await pollFor(ctx.page, 'window._imageAddedCount', 1);
await ctx.proxy.write(SIXEL_SEQ_0);
await pollFor(ctx.page, 'window._imageAddedCount', 2);
await ctx.proxy.write(SIXEL_SEQ_0);
await pollFor(ctx.page, 'window._imageAddedCount', 3);
});
test('delete image once scrolled off', async () => {
await ctx.proxy.write(SIXEL_SEQ_0);
pollFor(ctx.page, 'window.imageAddon._storage._images.size', 1);
@@ -272,26 +290,158 @@ 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 getImageSize(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 getImageSize(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 getImageSize(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 getImageSize(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 getImageSize(1), TESTDATA_IIP[1][1]);
});
});
test.describe('IIP support - testimages', () => {
test('palette.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[0][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[0][1]);
});
test('spinfox.png', async () => {
await ctx.proxy.write(TESTDATA_IIP[1][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[1][1]);
});
test('w3c gif', async () => {
await ctx.proxy.write(TESTDATA_IIP[2][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[2][1]);
});
test('w3c jpeg', async () => {
await ctx.proxy.write(TESTDATA_IIP[3][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[3][1]);
});
test('w3c png', async () => {
await ctx.proxy.write(TESTDATA_IIP[4][0]);
deepStrictEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
deepStrictEqual(await getImageSize(1), TESTDATA_IIP[4][1]);
});
});
test.describe('IIP - QOI support', () => {
test('palette should yield same bytes from PNG and QOI', async () => {
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=525:${PALETTE_PNG_BASE64}\x07`);
deepStrictEqual(await getImageSize(1), [640, 80]);
await ctx.proxy.write(`\x1b[10H\x1b]1337;File=inline=1;size=${qoiData.length}:${PALETTE_QOI_BASE64}\x07`);
deepStrictEqual(await getImageSize(2), [640, 80]);
const pngScrape = await getImageAtBufferCell(0, 0);
const qoiScrape = await getImageAtBufferCell(0, 11);
deepStrictEqual(qoiScrape, pngScrape);
});
});
test.describe('IIP - resizing', () => {
/**
* The correct resize behavior is wonky and needs to be tested against iTerm2,
* especially for missing params to derive correct default behavior.
* We document the current behavior here w'o claiming to be fully in line with iTerm2.
* ref: https://iterm2.com/documentation-images.html
* imgcat: https://iterm2.com/utilities/imgcat
*
* NOTE: QOI has a slightly different parse path, thus we test resizing explicitly
*/
const images = [
['palette.png', 525, PALETTE_PNG_BASE64],
['palette.qoi', qoiData.length, PALETTE_QOI_BASE64]
];
for (const [name, size, payload] of images) {
test(name + ': N --> width=20 height=5 preserveAspectRatio=0', async () => {
// cell based resize
const header = 'width=20;height=5;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions();
deepStrictEqual(await getImageSize(1), [dim.cellWidth * 20, dim.cellHeight * 5]);
});
test(name + ': Npx --> width=320px height=160px preserveAspectRatio=0', async () => {
// pixel based resize
const header = 'width=320px;height=160px;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
deepStrictEqual(await getImageSize(1), [320, 160]);
});
test(name + ': N% --> width=50% height=30% preserveAspectRatio=0', async () => {
// % of viewport resize
const header = 'width=50%;height=30%;preserveAspectRatio=0';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions();
deepStrictEqual(await getImageSize(1), [Math.floor(dim.width * 0.5), Math.floor(dim.height * 0.3)]);
});
test(name + ': ommitted dimension assumes preserveAspectRatio=1', async () => {
// width provided in percent
const header = 'width=50%';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header}:${payload}\x07`);
const dim = await getDimensions();
const width = Math.floor(dim.width * 0.5);
deepStrictEqual(await getImageSize(1), [width, Math.floor(width * 80 / 640)]);
// height provided in pixel
const header2 = 'height=200px';
await ctx.proxy.write(`\x1b]1337;File=inline=1;size=${size};${header2}:${payload}\x07`);
deepStrictEqual(await getImageSize(2), [Math.floor(200 * 640 / 80), 200]);
});
}
});
test.describe('text overwrite removes tiles', () => {
test('sixel', async () => {
await assertTextClearsTiles(SIXEL_SEQ_0);
});
test('iip', async () => {
await assertTextClearsTiles(TESTDATA_IIP[0][0]);
});
});
});
@@ -300,12 +450,12 @@ test.describe('ImageAddon', () => {
* terminal access helpers.
*/
async function getDimensions(): Promise<IDimensions> {
const dimensions: any = await ctx.page.evaluate(`term._core._renderService.dimensions`);
const dimensions: any = await ctx.page.evaluate(`term.dimensions`);
return {
cellWidth: Math.round(dimensions.css.cell.width),
cellHeight: Math.round(dimensions.css.cell.height),
width: Math.round(dimensions.css.canvas.width),
height: Math.round(dimensions.css.canvas.height)
cellWidth: Math.round(dimensions.device.cell.width),
cellHeight: Math.round(dimensions.device.cell.height),
width: Math.round(dimensions.device.canvas.width),
height: Math.round(dimensions.device.canvas.height)
};
}
@@ -321,9 +471,30 @@ async function getScrollbackPlusRows(): Promise<number> {
return ctx.page.evaluate('window.term.options.scrollback + window.term.rows');
}
async function getOrigSize(id: number): Promise<[number, number]> {
async function getImageSize(id: number): Promise<[number, number]> {
return ctx.page.evaluate<any>(`[
window.imageAddon._storage._images.get(${id}).orig.width,
window.imageAddon._storage._images.get(${id}).orig.height
]`);
window.imageAddon._storage._images.get(${id}).src.width * window.imageAddon._storage._images.get(${id}).prescaleX,
window.imageAddon._storage._images.get(${id}).src.height * window.imageAddon._storage._images.get(${id}).prescaleY
]`).then(r => r.map(Math.floor));
}
async function getImageAtBufferCell(x: number, y: number): Promise<string | undefined> {
return ctx.page.evaluate<any>(`window.imageAddon.getImageAtBufferCell(${x}, ${y})?.toDataURL('image/png')`);
}
async function hasTileAtBufferCell(x: number, y: number): Promise<boolean> {
return ctx.page.evaluate(`!!window.imageAddon.getImageAtBufferCell(${x}, ${y})`);
}
async function assertTextClearsTiles(imageSeq: string): Promise<void> {
await ctx.proxy.write('\x1b[H' + imageSeq);
await pollFor(ctx.page, '!!window.imageAddon.getImageAtBufferCell(5, 1)', true);
ok(await hasTileAtBufferCell(0, 1));
await ctx.proxy.write('\x1b[2;6H#######');
for (let x = 5; x < 12; x++) {
strictEqual(await hasTileAtBufferCell(x, 1), false);
}
ok(await hasTileAtBufferCell(0, 1));
ok(await hasTileAtBufferCell(13, 1));
ok(((await ctx.page.evaluate('window.term.buffer.active.getLine(1).translateToString(true)')) as string).includes('#######'));
}
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -5,10 +5,9 @@ const config: PlaywrightTestConfig = {
timeout: 10000,
projects: [
{
name: 'ChromeStable',
name: 'Chromium',
use: {
browserName: 'chromium',
channel: 'chrome'
browserName: 'chromium'
}
},
{
+3 -1
View File
@@ -9,13 +9,15 @@
"outDir": "../out-test",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
],
"*": [
"./*"
]
},
"strict": true,
+15 -6
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal, ITerminalAddon } from '@xterm/xterm';
import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm';
declare module '@xterm/addon-image' {
export interface IImageAddonOptions {
@@ -76,6 +76,15 @@ declare module '@xterm/addon-image' {
iipSupport?: boolean;
/** IIP sequence size limit (default 20000000 bytes). */
iipSizeLimit?: number;
/**
* Kitty graphics protocol settings
*/
/** Whether Kitty graphics protocol is enabled (default is true). */
kittySupport?: boolean;
/** Kitty image size limit in bytes (default 20000000 bytes). */
kittySizeLimit?: number;
}
export class ImageAddon implements ITerminalAddon {
@@ -107,14 +116,14 @@ declare module '@xterm/addon-image' {
*/
public showPlaceholder: boolean;
/**
* Event fired whenever a new image is added to storage.
*/
public readonly onImageAdded: IEvent<void>;
/**
* Get original image canvas at buffer position.
*/
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
/**
* Extract single tile canvas at buffer position.
*/
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
}
}
-2
View File
@@ -3,11 +3,9 @@ node_modules/
coverage/
lib/
fonts/
.env
.vscode/
*.swp
*.tgz
npm-debug.log*
yarn-error.log*
+27 -3
View File
@@ -1,6 +1,30 @@
MIT License
Copyright (c) 2018
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
The code that analyzes font ligatures is forked from https://github.com/princjef/font-ligatures with this license:
MIT License
Copyright (c) 2018 Jeffrey Principe
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
@@ -1,78 +0,0 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
const fs = require('fs');
const path = require('path');
const util = require('util');
const axios = require('axios').default;
const mkdirp = require('mkdirp');
const yauzl = require('yauzl');
const urls = {
fira: 'https://github.com/tonsky/FiraCode/raw/d42e7276fa925e5f82748f3ec9ea429736611b48/distr/otf/FiraCode-Regular.otf',
iosevka: 'https://github.com/be5invis/Iosevka/releases/download/v1.14.3/01-iosevka-1.14.3.zip'
};
const writeFile = util.promisify(fs.writeFile);
const fontsFolder = path.join(__dirname, '../fonts');
async function download() {
await mkdirp(fontsFolder);
try {
await downloadFiraCode();
await downloadIosevka();
console.log('Loaded all fonts for testing')
} catch (e) {
console.warn('Fonts failed to download, ligature tests will not work', e);
}
}
async function downloadFiraCode() {
const file = path.join(fontsFolder, 'firaCode.otf');
if (await util.promisify(fs.exists)(file)) {
console.log('Fira Code already loaded');
} else {
console.log('Downloading Fira Code...');
await writeFile(
file,
(await axios.get(urls.fira, { responseType: 'arraybuffer' })).data
);
}
}
async function downloadIosevka() {
const file = path.join(fontsFolder, 'iosevka.ttf');
if (await util.promisify(fs.exists)(file)) {
console.log('Iosevka already loaded');
} else {
console.log('Downloading Iosevka...');
const iosevkaContents = (await axios.get(urls.iosevka, { responseType: 'arraybuffer' })).data;
const iosevkaZipfile = await util.promisify(yauzl.fromBuffer)(iosevkaContents);
await new Promise((resolve, reject) => {
iosevkaZipfile.on('entry', entry => {
if (entry.fileName === 'ttf/iosevka-regular.ttf') {
iosevkaZipfile.openReadStream(entry, (err, stream) => {
if (err) {
return reject(err);
}
const writeStream = fs.createWriteStream(file);
stream.pipe(writeStream);
writeStream.on('close', () => resolve());
});
}
});
});
}
}
download();
process.on('unhandledRejection', e => {
console.error(e);
process.exit(1);
});
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8 -15
View File
@@ -1,6 +1,6 @@
{
"name": "@xterm/addon-ligatures",
"version": "0.9.0",
"version": "0.10.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -14,14 +14,13 @@
"node": ">8.0.0"
},
"scripts": {
"prepare": "node bin/download-fonts.js",
"build": "tsc -p src",
"watch": "tsc -w -p src",
"build": "tsgo -p src",
"watch": "tsgo -w -p src",
"prepackage": "npm run build",
"package": "webpack",
"pretest": "npm run build",
"test": "nyc mocha out/**/*.test.js",
"prepublish": "npm run package"
"prepublishOnly": "npm run package"
},
"keywords": [
"font",
@@ -32,17 +31,11 @@
],
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.4.1"
"lru-cache": "^11.3.6",
"opentype.js": "^2.0.0"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
"axios": "^1.6.0",
"mkdirp": "0.5.5",
"sinon": "6.3.5",
"yauzl": "^2.10.0"
},
"peerDependencies": {
"@xterm/xterm": "^5.0.0"
"@types/opentype.js": "^1.3.9",
"font-finder": "^1.1.0"
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ export interface ITerminalAddon {
dispose(): void;
}
export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
export class LigaturesAddon implements ITerminalAddon, ILigaturesApi {
private readonly _fallbackLigatures: string[];
private readonly _fontFeatureSettings?: string;
@@ -22,7 +22,7 @@ export class LigaturesAddon implements ITerminalAddon , ILigaturesApi {
constructor(options?: Partial<ILigatureOptions>) {
// Source: calt set from https://github.com/be5invis/Iosevka?tab=readme-ov-file#ligations
this._fallbackLigatures = (options?.fallbackLigatures || [
this._fallbackLigatures = (options?.fallbackLigatures ?? [
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '::', ':::',

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