Compare commits

...
929 Commits
Author SHA1 Message Date
Daniel Imms ac923e55d2 Merge pull request #4553 from Tyriar/v5_2_1
v5.2.1
2023-06-10 14:01:33 -07:00
Daniel Imms e08e5607ed v5.2.1 2023-06-10 13:59:51 -07:00
Daniel Imms 2ac2eeadfc Merge pull request #4551 from Tyriar/npes
Fix NPEs on buffer line and windowsPty option
2023-06-09 08:59:24 -07:00
Daniel Imms ffadc0b31d Fix NPEs on buffer line and windowsPty option 2023-06-09 08:47:17 -07:00
Daniel Imms cc1089e943 Merge pull request #4549 from Tyriar/r5_2_0
v5.2.0
2023-06-08 14:43:25 -07:00
Daniel Imms 47c21df83d v5.2.0 2023-06-08 14:32:54 -07:00
Daniel Imms 380e6805e8 Merge pull request #4539 from Tyriar/tyriar/windowsPty
Add more granular windowsPty option
2023-06-07 11:41:13 -07:00
Daniel Imms 9f424833d6 Merge pull request #4546 from jerch/fix_4545
DOM renderer link underline
2023-06-05 05:20:54 -07:00
Jörg Breitbart 90ccc815cb more verbose comments to get the idea of the code 2023-06-05 12:23:56 +02:00
Jörg Breitbart 8de7bd5850 rename lneX to maxY 2023-06-04 21:11:55 +02:00
Jörg Breitbart 9fed8e2a3a defensive underline event coords handling in DOM renderer 2023-06-04 21:06:26 +02:00
Daniel Imms 118915c1f7 Merge pull request #4542 from silamon/node18
Upgrade to Node18
2023-05-31 10:50:12 -07:00
Simon Lamon fca593d93b Fix unit testing (headless) 2023-05-31 17:37:41 +00:00
Simon Lamon 3376ee2f85 Node 18 2023-05-31 17:14:49 +00:00
Daniel Imms f6fbf63673 Add windowsPty unit test 2023-05-29 09:50:52 -07:00
Daniel Imms 3e5c6a156b Correct Windows wrapping heuristics condition 2023-05-29 09:49:33 -07:00
Daniel Imms a91665cb18 Listen for windowsPty changes 2023-05-29 09:43:49 -07:00
Daniel Imms 22140b5e94 Add more granular windowsPty option
See microsoft/vscode#134448
2023-05-29 09:40:00 -07:00
Daniel Imms 22a4dc4b6b Merge pull request #4478 from jerch/fix_4469
fix DECRQM reports
2023-05-24 06:47:52 -07:00
Megan Rogge 0ff06db417 bring back accessibility tree (#4536) 2023-05-22 21:17:25 +00:00
Daniel Imms 83a77b6024 Merge pull request #4533 from Tyriar/4480
Workaround drawing off invalid bitmap in canvas
2023-05-21 09:13:18 -07:00
Daniel Imms 8b422442b7 Close generator before deleting it 2023-05-21 09:05:21 -07:00
Daniel Imms 02ce3eff87 Workaround drawing off invalid bitmap in canvas
Fixes #4480
2023-05-21 08:51:31 -07:00
Daniel Imms 518b87f01d Merge pull request #4532 from Tyriar/4529
Finish overline support
2023-05-21 08:25:20 -07:00
Daniel Imms 6188c07bf7 Serialize overline
Fixes #4529
2023-05-21 08:17:06 -07:00
Daniel Imms 325d133a75 Add overline to vt comments
Part of #4529
2023-05-21 08:16:57 -07:00
Daniel Imms b662824b81 Add isOverline to public API
Part of #4529
2023-05-21 08:16:38 -07:00
Daniel Imms ada5bfe048 Merge pull request #4530 from jerch/fix_atlas_merge_webgl
fix blackout lines in webgl
2023-05-21 07:52:38 -07:00
Jörg Breitbart f270127ce5 fix webgl part of #4480 2023-05-21 16:32:57 +02:00
Daniel Imms 7460782494 Merge pull request #4527 from jerch/fix_4485
fix buffer corruption for utf8 transport in demo
2023-05-21 07:03:21 -07:00
Jörg Breitbart 048010c219 fix buffer corruption for utf8 transport 2023-05-21 12:28:15 +02:00
Daniel Imms cdec1e5b03 Merge pull request #4526 from Tyriar/4499_overline
Add support for SGR 53 overline and SGR 55 not overline
2023-05-20 14:21:22 -07:00
Daniel Imms eb56356133 Don't dodge overline, simplify drawing 2023-05-20 10:04:14 -07:00
Daniel Imms 2bfd95fe54 Add support for SGR 53, 55 overline
Specced in ECMA-48
2023-05-20 09:56:56 -07:00
Daniel Imms e9b0593298 Merge pull request #4525 from Tyriar/4161_dim
Don't apply dim to background color
2023-05-20 09:43:16 -07:00
Daniel Imms d5c47ec531 it -> itWebgl 2023-05-20 09:35:37 -07:00
Daniel Imms 5a145fcffe Add webgl api tests for dim 2023-05-20 08:41:11 -07:00
Daniel Imms 539fed4daa Don't apply dim to background color
Fixes #4161
2023-05-20 08:13:50 -07:00
Daniel Imms a0493a604c Merge pull request #4521 from jeanp413/jp/fix-forced-reflow
Avoid triggering a reflow while rendering search decorations
2023-05-17 16:04:16 -07:00
Jean Pierre 07e8f86bd1 Avoid triggering a reflow 2023-05-17 17:03:25 -05:00
Daniel Imms 1dbc1bf639 Merge pull request #4516 from jeanp413/jp/fix-decoration-render-layer
Ensure decorations in the top layer render on top of decorations in the bottom layer
2023-05-17 07:08:47 -07:00
Daniel Imms 6e8c59f0db Merge pull request #4504 from jeanp413/jp/fix-4444
Fixes improve search addon behavior when there are > 1000 results
2023-05-17 07:02:29 -07:00
Daniel Imms ec227c757b Prefix class with xterm-decoration- 2023-05-17 06:57:13 -07:00
Jean Pierre 4fbeb58203 Update search demo 2023-05-15 22:18:36 -05:00
Jean Pierre 60f549028f Ensure decorations in the top layer render on top of decorations in the bottom layer 2023-05-12 16:19:03 -05:00
Jean Pierre cc5136aeb3 Address feedback 2023-05-12 16:08:11 -05:00
Daniel Imms a1e9105ce7 Merge branch 'master' into jp/fix-4444 2023-05-12 06:24:34 -07:00
Daniel Imms 7a7f4f0add Merge pull request #4512 from xtermjs/Tyriar-patch-1
Add FAQ to new issue report
2023-05-10 06:40:53 -07:00
Daniel Imms 96ea9fe188 Add FAQ to new issue report
Hopefully this will reduce issue count a little
2023-05-10 06:31:54 -07:00
Jean Pierre b4e2293dbc Fix incremental 2023-05-09 16:14:28 -05:00
Daniel Imms 7194223696 Merge pull request #4508 from Tyriar/4507_zalgo
Disallow glyphs to exceed atlas texture size
2023-05-09 07:44:34 -07:00
Daniel Imms 8c9e44c219 Disallow glyphs to exceed atlas texture size
Fixes #4507
2023-05-09 07:35:41 -07:00
Daniel Imms 075f33031d Merge pull request #4475 from kelvinhammond/patch-1
Re-render browser DOM style onOptionsChanged, fixes xtermjs#4474
2023-05-09 07:12:58 -07:00
Jean Pierre e4b670231d Add tests 2023-05-08 09:08:35 -05:00
Jean Pierre 1c19f1d282 Remove incremental? 2023-05-07 15:19:23 -05:00
Jean Pierre 508589aa80 Improve search addon behavior when there are > 1000 results 2023-05-07 15:19:23 -05:00
Daniel Imms 854e2736f6 Move inject into _handleOptionsChanged 2023-05-07 06:27:56 -07:00
Kelvin 58f650e6f0 Merge branch 'master' into patch-1 2023-05-04 15:32:29 -04:00
Daniel Imms 913cb25863 Merge pull request #4491 from jerch/fix_4477
fix urls with screenreader
2023-05-03 08:19:23 -07:00
Daniel Imms 16f82cb3b5 Merge pull request #4500 from Tyriar/4487
Clear whole cursor layer on Firefox
2023-05-02 15:54:23 -07:00
Daniel Imms 0a101a01f5 Clear whole cursor layer on Firefox
Fixes #4487
2023-05-02 15:44:36 -07:00
Jörg Breitbart 9638063359 fix #4477 2023-04-24 12:30:41 +02:00
Jörg Breitbart ee26780e03 fix test cases 2023-04-16 21:17:12 +02:00
Jörg Breitbart b35415ed16 flip SET 2 & 12 perma flags, add DECSET 67 (DECBKM) as perma reset 2023-04-16 21:08:42 +02:00
Kelvin 2a9ec8fd1d Re-render browser DOM style onOptionsChanged, fixes xtermjs#4474
Fixes https://github.com/xtermjs/xterm.js/issues/4474
2023-04-11 13:43:32 -04:00
Daniel Imms 2fdb46919c Merge pull request #4465 from Tyriar/4410
Ensure Decoration.isDisposed can be true
2023-03-31 12:21:52 -07:00
Daniel Imms c8e633ecb5 Merge pull request #4464 from Tyriar/4409
Correct registerMarker docs
2023-03-31 12:12:19 -07:00
Daniel Imms c546f7cc8a Ensure Decoration.isDisposed can be true
Fixes #4410
2023-03-31 12:11:50 -07:00
Daniel Imms 6997f1a940 Correct registerMarker docs
Fixes #4409
2023-03-31 11:55:30 -07:00
Daniel Imms 14c6df3187 Merge pull request #4458 from Tyriar/tyriar/178014
Clear line wrapped state on explicit line feed
2023-03-28 11:46:45 -07:00
Daniel Imms 18be346df9 Clear line wrapped state on explicit line feed
Fixes #4457
See microsoft/vscode#178014
2023-03-28 11:35:21 -07:00
Daniel Imms 37297e09ed Merge pull request #4456 from xtermjs/Tyriar-patch-1
Add DOM term to real world use cases
2023-03-28 09:04:25 -07:00
Daniel Imms b2f0cc8dc1 Add DOM term to real world use cases
Fixes #4455
2023-03-28 08:51:24 -07:00
Daniel Imms 6cbee3349e Merge pull request #4451 from Starwort/master
Fix #4450
2023-03-27 06:36:53 -07:00
Starwort f0edc1b613 Fix #4450 2023-03-27 09:56:17 +01:00
Daniel Imms cd9867e222 Merge pull request #4443 from tisilent/#4441
Uniform cursor unfocused rendering. #4441
2023-03-23 07:58:55 -07:00
tisilent e1d852c86a Adjust DomRenderer cursor style 2023-03-22 21:09:28 +08:00
tisilent 455c744ab1 Uniform cursor unfocused rendering. #4441 2023-03-22 20:54:58 +08:00
Daniel Imms 21f48ba803 Merge pull request #4438 from xtermjs/jerch-patch-1
fix BCE render bug in canvas renderer
2023-03-15 07:51:45 -07:00
jerch 49fdacba6a fix BCE render bug in canvas renderer 2023-03-15 15:41:44 +01:00
Daniel Imms a359c7ca10 Merge pull request #4437 from xtermjs/dependabot/npm_and_yarn/webpack-5.76.0
Bump webpack from 5.61.0 to 5.76.0
2023-03-15 06:31:53 -07:00
dependabot[bot] 242eade757 Bump webpack from 5.61.0 to 5.76.0
Bumps [webpack](https://github.com/webpack/webpack) from 5.61.0 to 5.76.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.61.0...v5.76.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 15:58:20 +00:00
Megan Rogge 4037b943b9 remove AccessibleBuffer (#4427)
* remove AccessibleBuffer

* rm more

* Update src/browser/Terminal.ts

* rm css
2023-03-02 14:51:32 -06:00
Megan Rogge 9154831afa allow html elements (#4413) 2023-02-15 14:34:00 -06:00
Megan Rogge d6599bc048 accessibilityBuffer -> accessibleBuffer (#4406) 2023-02-08 21:48:20 +00:00
Megan Rogge 8a1faed5cb ensure line is defined via clamp (#4403) 2023-02-07 21:03:06 +00:00
Megan Rogge 72a157df5e add registerBufferElementProvider (#4402) 2023-02-07 09:56:23 -08:00
Megan Rogge 6daadc4c55 pull AccessibleBuffer out of AccessibilityManager (#4400) 2023-02-03 18:59:30 +00:00
Daniel Imms c05d0f650c Merge pull request #4398 from Tyriar/tyriar/a11y_remove
Remove accessibility tree code
2023-02-02 11:47:17 -08:00
Daniel Imms d5b5f1a9a4 Clean up 2023-02-02 06:24:18 -08:00
Daniel Imms dfcd674f8c Fix const -> let in LocalizableStrings
This needs to be settable outside, so const gives the wrong impression
2023-02-02 06:18:09 -08:00
Daniel Imms a7e0a9c534 Remove old accessibility tree code 2023-02-02 06:17:32 -08:00
Daniel Imms 5839accbf1 Use is prefix for boolean prop/getter 2023-02-02 06:12:04 -08:00
Megan Rogge 01c9a98e8e update accessibilityBufferActive on focusOut (#4395) 2023-02-01 20:57:52 +00:00
Daniel Imms bcfed71e1d Merge pull request #4393 from Tyriar/createinstance
Improve createInstance types
2023-02-01 12:38:30 -08:00
Daniel Imms 046d4b8314 Improve createInstance types 2023-02-01 08:42:51 -08:00
Megan Rogge a9da61e605 accessibilityBuffer: textarea -> div & prevent mouse click from exiting (#4389) 2023-01-31 18:25:25 -06:00
Daniel Imms 24c7be210c Merge pull request #4390 from Tyriar/webgl_exception
Handle glyph/rect renderer safer
2023-01-31 11:25:41 -08:00
Daniel Imms 9c815250b6 Handle glyph/rect renderer safer
See microsoft/vscode#172782
2023-01-31 11:14:33 -08:00
Megan Rogge 377f88796f append accessibility tree as child, not as adjacent elt to terminal (#4383) 2023-01-24 19:40:46 +00:00
Megan Rogge ed6c849501 attach and detach accessibility tree root element (#4382) 2023-01-24 09:53:10 -06:00
Megan Rogge 5a21948b4a make accessibility buffer readonly (#4381) 2023-01-23 19:03:19 +00:00
Megan Rogge b24bda92ad use textbox instead of div for accessibility buffer (#4377) 2023-01-20 17:36:25 +00:00
Megan Rogge 9895691e0e use content editable (#4376) 2023-01-19 21:30:58 +00:00
Megan Rogge 77870b06b9 fix #4374 (#4375) 2023-01-19 16:49:53 +00:00
Daniel Imms 41dc93a008 Prototype improved a11y buffer view (#4340) 2023-01-18 23:00:21 +00:00
Daniel Imms ba9f2e0f11 Merge pull request #4364 from Tyriar/dom_remove
Replace dom helper with HTMLElement.remove()
2023-01-05 09:22:55 -08:00
Daniel Imms b3fb8db6b3 Replace dom helper with HTMLElement.remove() 2023-01-05 09:14:16 -08:00
Daniel Imms 91946dcd9b Merge pull request #4363 from xtermjs/dependabot/npm_and_yarn/json5-2.2.3
Bump json5 from 2.1.2 to 2.2.3
2023-01-05 08:23:59 -08:00
dependabot[bot] 41c0e26292 Bump json5 from 2.1.2 to 2.2.3
Bumps [json5](https://github.com/json5/json5) from 2.1.2 to 2.2.3.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v2.1.2...v2.2.3)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-01-05 16:15:34 +00:00
Daniel Imms fe89817667 Merge pull request #4361 from xtermjs/circularlist-cleanup
Removed unused imports in CircularList
2022-12-28 07:19:26 -08:00
Eugene 7ee7aa2719 removed unused imports in CircularList 2022-12-28 15:41:58 +01:00
Daniel Imms b05c26c6fd Merge pull request #4358 from Tyriar/4357
Request links after viewport resize
2022-12-27 06:50:53 -08:00
Daniel Imms 0adbedf98d Request links after viewport resize
This clears the underline when hovering the link but this is an edge
case that isn't so important. As it's difficult to do outside of the
demo.

Fixes #4357
2022-12-27 06:41:58 -08:00
Daniel Imms 89ead0b4b7 Merge pull request #4356 from Coding-Kiwi/Coding-Kiwi-patch-1
Add graSSHopper to Real-world uses
2022-12-27 03:56:47 -08:00
Erik 349510065b Add graSSHopper to Real-world uses 2022-12-26 20:48:44 +01:00
Daniel Imms fa8a0e7f24 Merge pull request #4355 from Tyriar/4354
Clear canvas selection state after a resize
2022-12-24 08:10:44 -08:00
Daniel Imms 0c4e9c6b27 Merge pull request #4347 from silamon/deprecatedubuntu
Switch to Ubuntu 20.04
2022-12-24 08:06:14 -08:00
Daniel Imms de2b62eeac Merge pull request #4352 from Tyriar/4096
Include right part of wide char in underline gap
2022-12-24 08:05:41 -08:00
Daniel Imms 65b2af10f8 Clear canvas selection state after a resize
Fixes #4354
2022-12-24 08:02:41 -08:00
Daniel Imms 1a20fbb25f Merge pull request #4353 from Tyriar/4057
Force canvas renderer selection redraw on theme change
2022-12-24 08:02:06 -08:00
Daniel Imms d51ba2578b Force canvas renderer selection redraw on theme change
Fixes #4057
2022-12-24 07:46:51 -08:00
Daniel Imms 56734874ac Include right part of wide char in underline gap
Fixes #4096
2022-12-24 07:30:26 -08:00
Daniel Imms 72f1d1048a Merge pull request #4350 from Tyriar/dotted
Improve rendering of dotted underlines
2022-12-23 14:16:15 -08:00
Daniel Imms d17df65fad Improve rendering of dotted underlines
Fixes #4060
2022-12-23 14:04:12 -08:00
Simon Lamon c2504fa669 Ubuntu 18.04 deprecation 2022-12-23 17:37:26 +01:00
Daniel Imms 5a2bf6d039 Merge pull request #4346 from Tyriar/4314_2
Clamp start and end in render service to ensure they're valid
2022-12-21 17:03:15 -08:00
Daniel Imms 568e4c6935 Clamp start and end in render service to ensure they're valid
Fixes #4314
2022-12-21 16:55:03 -08:00
Daniel Imms 8b1c5c4ffb Revert "Fix NPE in webgl renderer"
This reverts commit ea7571c2b9.
2022-12-21 16:51:45 -08:00
Daniel Imms d34bb5ed79 Merge pull request #4343 from Tyriar/bump_other_addons
Bump other addons for v5.1 release
2022-12-21 08:28:46 -08:00
Daniel Imms ace4c3663f Bump other addons for v5.1 release
Fixes #4336
2022-12-21 08:27:01 -08:00
Daniel Imms 75075ebb70 Merge pull request #4342 from Tyriar/4341
Fix link event listener leak
2022-12-20 15:15:22 -08:00
Daniel Imms 7b8ad8c0b4 Improve comment 2022-12-20 15:01:30 -08:00
Daniel Imms f28c21eb77 Fix link event listener leak
The bug here is that _clearCurrentLink was being called only on the
line(s) that changed, but _askForLink was then being triggered
regardless. This caused more onRenderedViewportChange listeners to be
registered and for the thread blocking to get exponentially worse.

Fixes #4341
2022-12-20 14:58:35 -08:00
Daniel Imms 3142460db5 Merge pull request #4335 from jerch/cleanup_buffer
Cleanup Buffer.ts
2022-12-19 17:01:26 -08:00
Daniel Imms b97b1b6093 Merge pull request #4337 from Tyriar/whitespace_bump
Make package.json changes to trigger release
2022-12-19 12:55:19 -08:00
Daniel Imms 303db85763 Make package.json changes to trigger release
Fixes #4336
2022-12-19 12:55:01 -08:00
Jörg Breitbart 49ec7199e9 remover iterator and stringIndexToBufferIndex from buffer 2022-12-19 19:50:18 +01:00
Daniel Imms 19c760a204 Merge pull request #4333 from xtermjs/revert-4196-ansiheadless
Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal"
2022-12-19 10:20:24 -08:00
Daniel Imms 3a81ef54ca Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal" 2022-12-19 10:10:59 -08:00
Daniel Imms 6cfdcb1b0d Merge pull request #4332 from Tyriar/r5_1_0
v5.1.0
2022-12-19 09:56:50 -08:00
Daniel Imms 30ef1fcaa5 v5.1.0 2022-12-19 09:56:20 -08:00
Daniel Imms 84b8121105 Merge pull request #4288 from jerch/fix_weblinks
Fix weblinks
2022-12-19 08:57:15 -08:00
Jörg Breitbart 218cfd256c Merge branch 'master' into fix_weblinks 2022-12-19 15:59:11 +01:00
Jörg Breitbart 1757f2733c explicit type for link test data 2022-12-19 15:49:10 +01:00
Jörg Breitbart 9411acbc73 move link tests to demo 2022-12-19 15:33:10 +01:00
Daniel Imms cb4102cdcb Merge pull request #4330 from Tyriar/2533
Add VT buttons through CSI P
2022-12-19 06:30:11 -08:00
Daniel Imms 1926c9bbbc Add sequence buttons through CSI P 2022-12-19 06:20:01 -08:00
Daniel Imms 06145c02b1 Add IL vt button
Part of #2533
2022-12-19 06:11:34 -08:00
Jörg Breitbart d65898ffed Merge branch 'master' into fix_weblinks 2022-12-19 14:19:43 +01:00
Daniel Imms b5e6066f8c Merge pull request #4327 from Tyriar/2603
Improve IAttributeData jsdoc
2022-12-18 08:51:41 -08:00
Daniel Imms 0981ca8796 Merge pull request #4328 from Tyriar/vscode166909
Remove ! to improve safety
2022-12-16 11:16:17 -08:00
Daniel Imms ffeaa2b976 Remove ! to improve safety
See microsoft/vscode#166909

This doesn't necessarily fix that, but it does remove the hacky ! which could
potentially let an error slip past TS.
2022-12-16 11:05:03 -08:00
Daniel Imms 167765f932 Improve IAttributeData jsdoc
Fixes #2603
2022-12-16 10:53:21 -08:00
Daniel Imms bcdbb61ed4 Merge pull request #4326 from Tyriar/166878
Fix NPE in webgl renderer
2022-12-16 10:51:52 -08:00
Daniel Imms ea7571c2b9 Fix NPE in webgl renderer
Theory is this can happen when a resize down happens as end will be too big

See microsoft/vscode#166878
2022-12-16 10:42:05 -08:00
Daniel Imms 880b726bcb Merge pull request #4325 from Tyriar/4056
Fix canvas renderer selection not re-rendering sometimes
2022-12-16 09:39:28 -08:00
Daniel Imms 902da2800b Merge pull request #4324 from Ry0taK/feature/secure-links-by-default
Add allowNonHttpProtocols option to ILinkHandler
2022-12-16 09:32:54 -08:00
Daniel Imms 2bd9c4e839 Fix canvas renderer selection not re-rendering sometimes
Fixes #4056
2022-12-16 09:28:02 -08:00
RyotaK b3d36478ca Properly handle errors from URL constructor 2022-12-16 17:01:00 +00:00
RyotaK b07c016fea Adjust the comment 2022-12-16 16:58:55 +00:00
Daniel Imms 0b5625581e Merge pull request #4284 from JasonXJ/a11y-2
Fix a11y issues #4269
2022-12-16 08:57:55 -08:00
RyotaK e4b473785d Make comment more clear 2022-12-16 16:54:27 +00:00
RyotaK cb8d93a1ef Implement protocol validation 2022-12-16 16:50:57 +00:00
RyotaK 8636d0f820 Add allowNonHttpProtocols to typings 2022-12-17 01:31:11 +09:00
Daniel Imms f79644bd4c Merge pull request #4298 from jerch/fix_4295
re-eval previous active link
2022-12-16 08:12:19 -08:00
Daniel Imms 917e31ee17 Merge pull request #4322 from Tyriar/4124
Explain object option edge case
2022-12-15 17:06:24 -08:00
Daniel Imms aba59e9386 Explain object option edge case
Fixes #4124
2022-12-15 16:41:31 -08:00
Daniel Imms 1c7c4eb62f Merge pull request #4315 from Tyriar/2757
Allow scroll bar interaction in demo in screenReaderMode
2022-12-15 07:58:45 -08:00
Daniel Imms fc41968131 Allow scroll bar interaction in demo in screenReaderMode
Fixes #2757
2022-12-15 07:50:45 -08:00
Daniel Imms 04177de95b Merge pull request #4313 from Tyriar/4258
Support slash and triangle custom glyphs
2022-12-14 13:36:35 -08:00
Daniel Imms c9c07aeabd Support slash and triangle custom glyphs
This also fixes an issue with padding making glyphs regress

Fixes #4258
2022-12-14 13:15:05 -08:00
Daniel Imms 0cc759e0b1 Merge pull request #4312 from xtermjs/Tyriar-patch-1
Point new issue questions at GH discussions
2022-12-14 12:30:33 -08:00
Daniel Imms 852255ea03 Point new issue questions at GH discussions 2022-12-14 12:10:48 -08:00
Daniel Imms 88f459583e Merge pull request #4311 from Tyriar/vtbuttons
Add VT arrow buttons
2022-12-14 09:37:38 -08:00
Daniel Imms fd8a0690c9 Add VT arrow buttons
Part of #2064
2022-12-14 09:28:25 -08:00
Daniel Imms 6e2744a299 Merge pull request #4310 from Tyriar/4309
Improve explanation of danger with the linkHandler API
2022-12-14 08:21:55 -08:00
Daniel Imms 15e3b1ac12 Improve explanation of danger with the linkHandler API
Fixes #4309
2022-12-14 08:10:52 -08:00
Daniel Imms 5a5ed6d767 Merge pull request #4306 from Tyriar/4161
Add combo entries to sgr test for testing multiple sgr
2022-12-11 13:42:47 -08:00
Daniel Imms 74ea0c23ca Add combo entries to sgr test for testing multiple sgr 2022-12-11 13:11:16 -08:00
Daniel Imms 6d0a287ed9 Merge pull request #4305 from Tyriar/4221
Fix line height and letter spacing on canvas renderer
2022-12-11 11:54:57 -08:00
Daniel Imms 83ff4e97c3 Merge pull request #4304 from Tyriar/4259
Simplify options API
2022-12-11 11:52:08 -08:00
Daniel Imms 7b28dc4d59 Fix line height and letter spacing on canvas renderer
Fixes #4221
2022-12-11 11:47:09 -08:00
Daniel Imms 02d3ef599a Fix internal usages that needed Required<ITerminalOptions> 2022-12-11 11:42:14 -08:00
Daniel Imms c118f04187 Sync xterm-headless.d.ts 2022-12-11 11:36:32 -08:00
Daniel Imms 2bd3869547 Use a monospace font in example 2022-12-11 11:33:23 -08:00
Daniel Imms 69379ba59b Simplify options API
Fixes #4259
2022-12-11 11:33:03 -08:00
Daniel Imms 5ed05295ba Merge pull request #4301 from Tyriar/cleanup_coreservice
Avoid passing circular callback into CoreService ctor
2022-12-11 07:44:46 -08:00
Daniel Imms 4ee4a0f988 Merge branch 'master' into cleanup_coreservice 2022-12-11 07:36:01 -08:00
Daniel Imms d6b8a21219 Avoid passing circular callback into CoreService ctor 2022-12-11 07:35:11 -08:00
Daniel Imms 2e2d275d6c Merge pull request #4289 from JasonXJ/scroll-options
Add option scrollOnUserInput
2022-12-11 07:31:31 -08:00
Daniel Imms 33bba35be7 Merge pull request #4291 from jerch/fix_4223
fix demo in epiphany
2022-12-11 07:17:08 -08:00
jerch c7660fb49c Merge pull request #4299 from xtermjs/dependabot/npm_and_yarn/express-4.17.3
Bump express from 4.17.1 to 4.17.3
2022-12-10 20:50:46 +01:00
dependabot[bot] 6d527c42b0 Bump express from 4.17.1 to 4.17.3
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.17.3.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.17.3)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-10 17:06:32 +00:00
Jörg Breitbart 440da4a5ff reeval previous active link, fixes #4295 2022-12-09 16:37:56 +01:00
Jörg Breitbart e66fd50383 cleanup & more comments 2022-12-09 14:37:31 +01:00
Jörg Breitbart f69f9decca allow ~ within urls 2022-12-08 17:50:55 +01:00
Jörg Breitbart e70138b63c revert temp changes in linkifier2 2022-12-08 17:46:45 +01:00
Jörg Breitbart becec91e98 better regexp, fix offset issue in #4294 2022-12-08 17:35:45 +01:00
Jörg Breitbart 03dc1908a5 Merge branch 'master' into fix_weblinks 2022-12-08 15:19:08 +01:00
Jörg Breitbart d2298d84a0 move safari check to WebglAddon ctor 2022-12-07 23:56:43 +01:00
Jörg Breitbart 0b56b56c06 skipping renderer on NUL and SP 2022-12-07 22:55:45 +01:00
Daniel Imms 6d46b7b86f Merge pull request #4115 from jerch/buffer_optimizations
some buffer handling optimizations
2022-12-07 13:43:40 -08:00
Jörg Breitbart b829ad8386 Merge branch 'master' into fix_4223 2022-12-07 22:28:00 +01:00
Jörg Breitbart 9bc65c36a6 Merge branch 'master' into buffer_optimizations 2022-12-07 19:45:14 +01:00
Jörg Breitbart a562f33d53 remove nonsense exp runtime of rescans 2022-12-07 19:43:56 +01:00
Daniel Imms fe30cd6ed8 Merge pull request #4290 from JasonXJ/alt-space
Send corresponding escape code for alt+space and ctrl+alt+space
2022-12-07 06:34:23 -08:00
Jason Lin 9e84895141 polish 2022-12-07 10:12:43 +11:00
Daniel Imms 497df06f3f Merge pull request #4293 from Tyriar/4292
Log a warning if task queues exceed deadline by 20ms
2022-12-06 14:20:37 -08:00
Daniel Imms 650cda31fd Tweak logging 2022-12-06 14:10:40 -08:00
Daniel Imms dec3791f4c Log a warning if task queues exceed deadline by 20ms
Fixes #4292
2022-12-06 14:04:10 -08:00
Jörg Breitbart e46a9e12d0 fix demo in epiphany 2022-12-06 13:40:25 +01:00
Jason Lin 7f64fa9ddd Send corresponding escape code for alt+space and ctrl+alt+space
The previous behavior is that this combinations do nothing.
2022-12-06 15:57:19 +11:00
Jason Lin 73d20c1d3b Add option scrollOnKeypress
Note that we also change the existing behavior a bit:
CoreService.triggerDataEvent() will not scroll to the bottom unless
`wasUserInput` is also true. This actually seems to be the intended
behavior according to the doc just above
ICoreService.triggerDataEvent().
2022-12-06 14:29:18 +11:00
Jörg Breitbart fd44d76262 api tests for uri and ranges 2022-12-05 22:49:25 +01:00
Jörg Breitbart 309ba47483 change batch size to 500 2022-12-05 20:03:25 +01:00
Jörg Breitbart 4084dc3949 remove _forceMemoryCleanup 2022-12-05 19:10:55 +01:00
Jörg Breitbart 42bacc3283 disabled faulty code in linkifier2 2022-12-05 18:30:25 +01:00
Jörg Breitbart 46bd97247d disabled faulty code in linkifier2 2022-12-05 18:28:42 +01:00
Jörg Breitbart 3bf72cdefc DOM renderer fix with much better performance 2022-12-05 15:50:29 +01:00
Jörg Breitbart de23616589 better line expansion 2022-12-05 13:45:47 +01:00
Jörg Breitbart 39c2e57ad3 hack around DOM renderer issue, fix api tests 2022-12-04 22:26:29 +01:00
Jörg Breitbart d19361ac85 initial rewrite of addon mechs 2022-12-04 19:53:36 +01:00
Jörg Breitbart 1f5159febe use IdleTaskQueue & Date.now; some testcases 2022-12-03 16:49:26 +01:00
Jörg Breitbart 3a2ae96960 Merge branch 'master' into buffer_optimizations 2022-12-03 11:24:51 +01:00
Daniel Imms ad1f2ac2a8 Merge pull request #4287 from xtermjs/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-02 09:54:13 -08:00
dependabot[bot] b972e7f5ad Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 17:45:59 +00:00
Daniel Imms 503fe99b0a Merge pull request #4286 from Tyriar/tyriar/mocha
Update mocha
2022-12-02 09:45:34 -08:00
Daniel Imms 953cf005c0 Update mocha
This resolves a security alert
2022-12-02 09:35:36 -08:00
Jason Lin d177acccee Fix a11y issues #4269 2022-12-02 18:35:09 +11:00
Daniel Imms 4451369c66 Merge pull request #4280 from Tyriar/4278
Add custom keymap example to attachCustomKeyEventHandler docs
2022-11-30 07:15:12 -08:00
Daniel Imms 6b09c1b576 Add custom keymap example to attachCustomKeyEventHandler docs
Fixes #4278
2022-11-30 07:06:28 -08:00
Daniel Imms 71eb514bd0 Merge pull request #4276 from bugwheels94/bugwheels94-add-dependent-to-readme
Added application that uses xterm.js
2022-11-28 13:04:48 -08:00
Ankit cd552500fc Update README.md 2022-11-29 02:23:56 +05:30
Daniel Imms ed1773c103 Merge pull request #4274 from JasonXJ/cap-atlas-pages
Cap webgl maxAtlasPages to 32
2022-11-26 07:24:47 -08:00
Jason Lin b7c119a071 Cap webgl maxAtlasPages to 32 2022-11-24 17:51:58 +11:00
Daniel Imms 452241ca92 Merge pull request #4271 from jeanp413/fix-vscode-166158
Fixes disappearance of characters in webgl renderer
2022-11-23 09:36:15 -08:00
Daniel Imms 1880399849 dirtyId -> version 2022-11-23 08:51:06 -08:00
Jean Pierre d4ee0309d9 Fixes https://github.com/microsoft/vscode/issues/166158 2022-11-22 00:07:23 -05:00
Daniel Imms a351f5758a Merge pull request #4255 from Tyriar/safari16
Enable the webgl renderer on safari 16 and above
2022-11-08 12:30:23 -08:00
Daniel Imms 3597d43788 Enable the webgl renderer on safari 16 and above
This does not fix the issue where webkit browsers cannot use it, I looked
at webkit versions in safari and was a bit confused. The wikipedia page
for safari shows v15 having webkit v612+ but I have safari v16 and it's
webkit v605, so trying to detect it based on webkit version is a risky
change.

Note that webgl2 shipped in safari 15, but it didn't work in webgl until
safari 16.

Fixes #3357
Related #3575
Related microsoft/vscode#165416
2022-11-08 12:23:20 -08:00
Daniel Imms f381783bd5 Merge pull request #4196 from silamon/ansiheadless
SerializeAsHTML(): Fallback to default ansi colors when running headless terminal
2022-11-04 08:15:59 -07:00
Daniel Imms 6969a94fe4 Merge branch 'master' into ansiheadless 2022-11-04 08:08:43 -07:00
Daniel Imms 2377472eca Merge pull request #4216 from JasonXJ/fix-csi-0-m
'CSI 0 m' should clear the extended attribute
2022-11-03 19:58:18 -07:00
Daniel Imms 0edabed595 Merge pull request #4240 from Tyriar/4222_shims
Remove promise and fetch shims and use await for fetch
2022-11-03 19:56:25 -07:00
Daniel Imms 7d98375ce4 Merge pull request #4254 from Tyriar/clear_green
Clear custom glyph test button sgr
2022-11-03 06:56:23 -07:00
Daniel Imms 862e83d7be Merge pull request #4252 from Tyriar/4243_merge
Continually merge texture atlas pages
2022-11-03 06:48:55 -07:00
Daniel Imms 7cf9b68e2c Clear custom glyph test button sgr 2022-11-03 06:48:33 -07:00
Daniel Imms 97a0df260b Remove unused event 2022-11-03 05:44:50 -07:00
Daniel Imms c66dc82286 Merge pull request #4251 from Tyriar/4250
Add 1024 character limit to web links addon
2022-11-02 06:56:22 -07:00
Daniel Imms 6c29864847 Add 1024 character limit to web links addon
Fixes #4250
2022-11-02 06:45:25 -07:00
Daniel Imms d82da195f3 Remove atlas pages from demo when they're merged 2022-11-02 06:02:53 -07:00
Daniel Imms abfe391d91 Add CJK with random SGR test button
This is good as a stress test for the renderer
2022-11-02 05:46:40 -07:00
Daniel Imms c22e684904 Fix exception when last 4 smallest pages are merged 2022-11-02 05:40:13 -07:00
Daniel Imms 6406fd79c1 Enforce a maximum page merge amount 2022-11-02 05:28:47 -07:00
Daniel Imms 0271fdf49d Merge 4 largest pages 2022-11-02 04:49:02 -07:00
Daniel Imms 805b41b80f Keep merging default texture size pages 2022-11-01 10:26:48 -07:00
Daniel Imms c9bacc80d7 Merge when max atlas pages is reached 2022-11-01 10:08:00 -07:00
Daniel Imms 54ee698ba6 Basic merging working
Part of #4243
2022-11-01 09:07:38 -07:00
Daniel Imms 295e3c7f0c Merge pull request #4249 from Tyriar/4241_fill
Remove fill polyfill
2022-10-31 12:34:17 -07:00
Daniel Imms d219da480c Remove fill polyfill
Fixes #4241
2022-10-31 12:24:59 -07:00
Daniel Imms 02fa38c334 Merge pull request #4244 from Tyriar/multi_page_atlas
Support multiple texture atlas pages
2022-10-30 18:30:11 -07:00
Daniel Imms 4d05905736 Polish 2022-10-30 18:11:24 -07:00
Jason Lin 81a3c80bc4 polish 2022-10-31 11:07:42 +11:00
Jason Lin f6fabebf8d polish 2022-10-31 11:07:42 +11:00
Jason Lin b735e7f1c4 polish 2022-10-31 11:07:42 +11:00
Jason Lin 839271096e Don't reset the url 2022-10-31 11:07:42 +11:00
Jason Lin 6a087e9896 'CSI 0 m' should clear the extended attribute
This change also removes `_currentLinkId` from `InputHandler` and uses
`_curAttrData.extended.urlId` directly so that we don't need to worry
about keep them in sync any more.
2022-10-31 11:07:42 +11:00
Daniel Imms dda3f5f545 Properly dispose of atlas event 2022-10-30 15:00:27 -07:00
Daniel Imms 04041d030f Generate bitmaps for all atlas pages 2022-10-30 14:56:27 -07:00
Daniel Imms 095ceda639 Show canvas atlas pages in demo 2022-10-30 14:53:01 -07:00
Daniel Imms 0aa9fc5bb8 Add border around atlas pages 2022-10-30 12:00:55 -07:00
Daniel Imms e092ca70e1 Simplify page reuse/creation 2022-10-30 10:33:38 -07:00
Daniel Imms 0592837620 Fill pages to capacity 2022-10-30 10:21:17 -07:00
Daniel Imms fc03a1698b Share page creation code 2022-10-30 10:16:48 -07:00
Daniel Imms c9ef4a8eb9 Increase texture size as it nears the maximum texture unit
#4243 would be a better, but more complicated, solution to this
2022-10-30 09:20:17 -07:00
Daniel Imms 846573bb01 Support up to maximum texture units (eg. 16 on my machine) 2022-10-30 03:16:29 -07:00
Daniel Imms 0f70055d76 Add CJK test button 2022-10-30 03:04:58 -07:00
Daniel Imms 3f2ed42e73 Fix glyphs getting clipped on bottom row 2022-10-29 20:57:42 -07:00
Daniel Imms 2a3c2d9520 Make better use of atlas space 2022-10-29 20:33:27 -07:00
Daniel Imms 5e95475d6b Use current row when filling in old atlas pages 2022-10-29 19:50:44 -07:00
Daniel Imms 57a65e0858 Continute filling atlas page after new pages are added 2022-10-29 19:44:12 -07:00
Daniel Imms bfa9dd5060 Clean up 2022-10-29 18:09:18 -07:00
Daniel Imms 04f79aa7b4 Share page texture binding code 2022-10-29 14:33:50 -07:00
Daniel Imms 86ccd709bf Avoid setting uniforms every frame 2022-10-29 14:26:51 -07:00
Daniel Imms c257a7247e Only upload textures that change 2022-10-29 13:27:33 -07:00
Daniel Imms 0a49a5dde1 Make texture atlas presentation nicer for multiple pages 2022-10-29 13:01:41 -07:00
Daniel Imms c6e3cc007c Stop atlas flickering on demo 2022-10-29 12:49:11 -07:00
Daniel Imms 713baeb64d Base atlas page size on device pixel ratio 2022-10-29 12:45:20 -07:00
Daniel Imms efba2c9040 Get pages working, add to demo 2022-10-28 19:30:42 -07:00
Daniel Imms 67ef455515 Get a second page kind of working 2022-10-28 19:20:26 -07:00
Daniel Imms 975d52fab9 Remove deprecated prop 2022-10-28 19:12:01 -07:00
Daniel Imms 6b37187b4d Create AtlasPage 2022-10-28 19:08:33 -07:00
Daniel Imms 84ab6a49c4 Bring texturePage partially to atlas 2022-10-28 18:56:49 -07:00
Daniel Imms 4ebc75aae0 Merge remote-tracking branch 'upstream/master' into multi_page_atlas 2022-10-28 18:42:46 -07:00
Daniel Imms 54335ef54a Create texture page attribute 2022-10-28 18:16:55 -07:00
Daniel Imms 43d8db77f9 Merge pull request #4239 from Tyriar/4224_mcr_canvas_ydisp
Correctly offset mcr check on canvas renderer
2022-10-28 14:10:25 -07:00
Daniel Imms 5a4a46f130 Remove promise and fetch shims and use await for fetch
Fixes #4222
2022-10-28 14:09:39 -07:00
Daniel Imms b0672aac09 Correctly offset mcr check on canvas renderer
Fixes #4224
2022-10-28 14:02:58 -07:00
Daniel Imms fcf378b172 Merge pull request #4237 from Tyriar/italic_bounds
Fix clipping of italic emoji
2022-10-28 09:12:07 -07:00
Daniel Imms 25551a4b98 Merge pull request #4236 from Tyriar/console_image
Add console.image helper
2022-10-28 09:04:37 -07:00
Daniel Imms 67b3faef01 Use bbox padding and width consistently 2022-10-28 09:01:22 -07:00
Daniel Imms 8b545a3fe4 Fix bbox right scan start offset
This causes particularly wide glyphs like italic emoji to get clipped
even if they're within the allowed padding.

Fixes #4229
2022-10-28 08:57:41 -07:00
Daniel Imms 4f4ce0146d Add console.image helper
This is useful for printing canvases that aren't attached to the DOM
2022-10-28 08:54:57 -07:00
Daniel Imms e7b347a2ef Merge pull request #4235 from Tyriar/opt_server_buffer
Avoid GC pressure from server data buffer
2022-10-27 22:41:07 -07:00
Daniel Imms 7908b7cf62 Avoid GC pressure from server data buffer 2022-10-27 22:30:46 -07:00
Daniel Imms 392f8e9c98 Merge pull request #4234 from Tyriar/slow_isFocused
Cache ICoreBrowserService.isFocused per task
2022-10-27 17:00:18 -07:00
Daniel Imms b5cd4f1492 Cache ICoreBrowserService.isFocused per task
This is called for every selected cell when rendering which ends up
consuming a bunch of CPU due to the DOM calls.
2022-10-27 16:50:45 -07:00
Daniel Imms 4edfc12cbf Merge pull request #4233 from Tyriar/4113_resize
Fix resize in demo
2022-10-27 14:03:17 -07:00
Daniel Imms 19c1ee56d3 Fix resize in demo
A double resize could happen due to the mismatch in canvas vs device
pixel widths

Fixes #4113
2022-10-27 13:55:24 -07:00
Daniel Imms a4f8b07a7f Merge pull request #4226 from Tyriar/dropped_frames
Fix dropped frame issue
2022-10-21 13:10:30 -07:00
Daniel Imms 8d4d5bd54b Fix dropped frame issue
This ensures decorations are refreshed in the same frame when called from
an animation frame callback

Fixes #4225
2022-10-21 13:02:24 -07:00
Daniel Imms d5710afcb3 Merge pull request #4219 from Tyriar/4218_safari_bitmap
Disable canvas ImageBitmap optimization on Safari
2022-10-19 10:58:43 -07:00
Daniel Imms 8bd8195e30 Disable canvas ImageBitmap optimization on Safari
Fixes #4218
2022-10-19 10:03:01 -07:00
Daniel Imms ffe066dfef Merge pull request #4217 from Nokel81/fix-dom-renderering-jsdom-v2
Change clearing innerText to using replaceChildren to fix testing using jsdom
2022-10-19 07:21:30 -07:00
Sebastian Malton 11e90591f9 Change clearing innerText to using replaceChildren to fix testing using jsdom
Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-10-19 10:09:12 -04:00
Megan Rogge 0b84d17b18 revert decoration dispose changes (#4215) 2022-10-18 21:32:22 +00:00
Daniel Imms 376f0d9972 Merge pull request #4211 from hackermondev/patch-1
replit branding change
2022-10-17 07:45:06 -07:00
hackermon 478535d236 replit branding change 2022-10-17 09:49:54 -04:00
Daniel Imms 3f12d2925a Merge pull request #4208 from Tyriar/todo_attach_socket
Warn/throw on unexpected attach addon socket state
2022-10-17 03:48:31 -07:00
Daniel Imms 8e65ee8832 Merge pull request #4210 from Tyriar/todo_type_tabs
Fix a bunch of TODOs
2022-10-16 07:15:19 -07:00
Daniel Imms a7f0c3868c Merge pull request #4209 from Tyriar/todo_onData_event_test
Re-enable onData event test
2022-10-16 07:08:59 -07:00
Daniel Imms 96486c414b Acquire char size service better in webgl renderer 2022-10-16 07:04:34 -07:00
Daniel Imms b560d85553 Remove unneeded params as link provider is stable 2022-10-16 07:04:24 -07:00
Daniel Imms 7796b4101a Type Buffer.tabs 2022-10-16 06:58:20 -07:00
Daniel Imms a982484392 Re-enable onData event test 2022-10-16 06:55:04 -07:00
Daniel Imms e15c6b9f3e Warn/throw on unexpected attach addon socket state 2022-10-16 06:49:27 -07:00
Daniel Imms 807183bba6 Merge pull request #4207 from Tyriar/3925_dimensions
Improve names of IRenderDimensions interface members
2022-10-16 06:41:37 -07:00
Daniel Imms dc19628014 Fix width -> height 2022-10-16 06:34:12 -07:00
Daniel Imms ca68a5c59a Fix typo in test 2022-10-15 20:13:24 -07:00
Daniel Imms 22e8a2f8e7 Remove old dimension properties 2022-10-15 18:07:05 -07:00
Daniel Imms 171ba446bd Update other files 2022-10-15 15:32:02 -07:00
Daniel Imms 89e0b87eac Move to device/css in domrenderer 2022-10-15 14:22:04 -07:00
Daniel Imms 2c9e711cae Move all other refs in webgl over 2022-10-15 13:55:36 -07:00
Daniel Imms b0f1c38fd2 Move all other refs in canvas over 2022-10-15 13:44:52 -07:00
Daniel Imms b393008b54 Use new render dimensions in canvasrenderer 2022-10-15 13:19:04 -07:00
Daniel Imms 65f41a151e Use new render dimensions in webglrenderer 2022-10-15 13:15:12 -07:00
Daniel Imms ddc4e75583 Add new dimensions properties, helper creation function
Part of #3925
2022-10-15 10:31:43 -07:00
Daniel Imms 869ce3aa35 Merge pull request #4194 from silamon/fixmissingline
Capture data between terminal creation and connected to terminal in demo
2022-10-15 10:17:51 -07:00
Daniel Imms 16ea621a5f Merge pull request #4206 from Tyriar/eslint-jsdoc
Add eslint-plugin-jsdoc with minimal comment rules
2022-10-15 08:55:07 -07:00
Daniel Imms 5204374494 Add no-multi-asterisks rule 2022-10-15 08:43:06 -07:00
Daniel Imms 9f1a71485c Fix invalid jsdoc tags 2022-10-15 08:41:49 -07:00
Daniel Imms 49e0480f78 Add rule jsdoc/check-alignment 2022-10-15 08:39:32 -07:00
Daniel Imms 114924f3fa Add eslint-plugin-jsdoc and enable check-param-names 2022-10-15 08:38:14 -07:00
Daniel Imms aeba140c75 Merge pull request #4204 from silamon/proposedApiCheck
Remove check proposed api checks
2022-10-15 08:04:12 -07:00
Daniel Imms aea7680f76 Merge pull request #4205 from Tyriar/triangle_strip
Draw quads with triangle strip
2022-10-15 08:02:07 -07:00
Daniel Imms fb4a4e6127 Draw quads with triangle strip
This reduces the number of needed vertices from 6 to 4, it may not have a
noticeable performance impact but it's less data and simpler
2022-10-15 07:54:31 -07:00
Simon Lamon cc82f605fe delete clean up 2022-10-15 10:47:59 +00:00
Simon Lamon a055948937 unsentOutput instead of logs and dispose temporary listener 2022-10-15 10:45:23 +00:00
Simon Lamon 8246440795 Merge branch 'master' into fixmissingline 2022-10-15 10:11:40 +00:00
Simon Lamon 073aa6d439 Fix test "Proposed API check" 2022-10-15 10:25:39 +02:00
Simon Lamon 6bbe5a82a8 Remove check proposed api checks 2022-10-15 07:28:21 +00:00
Daniel Imms 1e6dfec4ae Merge pull request #4201 from Tyriar/4197_2
Remove clipImageData completely
2022-10-13 06:34:37 -07:00
Daniel Imms 5285b9ac45 Merge branch 'master' into 4197_2 2022-10-13 06:25:59 -07:00
Daniel Imms 50d1834871 Remove clipImageData completely
putImageData works a little differently to drawImage which confused me
for a bit, you need to offset the destination as putImageData draws the
'dirty' parts of the texture using the same source image dimensions

Fixes #4197
2022-10-13 06:25:13 -07:00
Daniel Imms b1f785879b Merge pull request #4200 from Tyriar/4197
Optimize _clipImageData
2022-10-12 10:24:28 -07:00
Daniel Imms 28fcffe46c Optimize _clipImageData
This makes clip image data around twice as fast, it's a little tricky to
measure it though. Changes:

- Work on Uint32Array instead of Uint8 so it's 1 assignment per pixel
  instead of 1 per channel.
- Perform the clipping in-place using the original image data to avoid
  allocation a new buffer.

Fixes #4197
2022-10-12 10:16:26 -07:00
Jörg Breitbart 14b99dca76 Merge branch 'master' into buffer_optimizations 2022-10-09 21:41:06 +02:00
Daniel Imms 1b13127f0b Merge pull request #4195 from Tyriar/4190
Create onSpecificOptionChange and onMultipleOptionChange helpers
2022-10-09 11:04:59 -07:00
Daniel Imms a37ea14d10 Use options service in renderers 2022-10-09 10:57:49 -07:00
Simon Lamon f8a5d3a91e Fallback to default_ansi_colors when running headless terminal 2022-10-09 17:16:06 +00:00
Daniel Imms 8d7137417b Split renderer safe vs unsafe api on activate 2022-10-09 10:12:19 -07:00
Daniel Imms e47296c1b9 Use options service directly in renderers 2022-10-09 09:28:18 -07:00
Daniel Imms 96b26b2e4a Merge pull request #4193 from silamon/fixdemoagain
Fix demo again
2022-10-09 09:23:07 -07:00
Daniel Imms 8ac88bc6bf Move Terminal option updates into owning components 2022-10-09 09:07:01 -07:00
Daniel Imms 7eead44048 Add onMultipleOptionChange 2022-10-09 08:14:56 -07:00
Daniel Imms 49a0904857 Move onSpecificOptionChange adoption 2022-10-09 08:07:43 -07:00
Simon Lamon a4c62792df revert formatting changes 2022-10-09 15:03:54 +00:00
Simon Lamon 1c345bef48 Handle onWillFire if it fails, so it loads dom renderer 2022-10-09 15:01:15 +00:00
Daniel Imms 415100e8b8 Adopt onSpecificOptionChange 2022-10-09 07:46:10 -07:00
Simon Lamon 627410a537 Add broken demo to investigate 2022-10-09 14:26:08 +00:00
Daniel Imms cad9c4164c onOptionChange/onSpecificOptionChange tests 2022-10-09 07:16:56 -07:00
Daniel Imms a87b6c3f2e Implement IOptionsService.onSpecificOptionChange
Part of #4190
2022-10-09 07:09:35 -07:00
Daniel Imms a2a855d747 Merge pull request #4192 from silamon/ubuntudeprecation
Switch to Ubuntu 20.04
2022-10-09 06:25:11 -07:00
Simon Lamon fb91b01217 Adds logs again in server.js 2022-10-09 12:16:26 +00:00
Simon Lamon bd661fdb07 Fix demo again 2022-10-09 11:29:33 +00:00
Simon Lamon f983b3cac1 Ubuntu 18.04 deprecation 2022-10-09 10:15:13 +00:00
Daniel Imms 0ba12301a6 Merge pull request #4188 from Tyriar/4184
Create theme service
2022-10-08 15:51:44 -07:00
Daniel Imms 97e084aa50 Remove document from test 2022-10-08 15:44:21 -07:00
Daniel Imms e0fb3b4d19 Remove ColorManager 2022-10-08 15:23:29 -07:00
Daniel Imms a1050d1a11 Merge ColorManager into ThemeService 2022-10-08 15:20:15 -07:00
Daniel Imms 7f9e3b1225 Remove resolved todo 2022-10-08 14:09:00 -07:00
Daniel Imms 48f15cb543 Remove remaining setColors methods 2022-10-08 14:07:28 -07:00
Daniel Imms 6e35bcd767 Use theme service in more places 2022-10-08 13:55:23 -07:00
Daniel Imms 80e474c6ad Viewport listen to theme service 2022-10-08 13:31:31 -07:00
Daniel Imms 4dee0c5d4b Build out theme service more 2022-10-08 12:15:21 -07:00
Daniel Imms a22ad76bd8 Merge pull request #4189 from Tyriar/ensure_canvas_changed
Ensure stale bitmap is not used when drawing new characters
2022-10-08 12:04:27 -07:00
Daniel Imms 4f5154fd21 Ensure stale bitmap is not used when drawing new characters
This would cause a problem where the first time a glyph is used it would
not be visible because it was using the old invalidated bitmap.
2022-10-08 11:56:29 -07:00
Daniel Imms cc6def57e4 Use readonly color set in canvas addon 2022-10-08 11:46:41 -07:00
Daniel Imms c4750eaf77 Create basic theme service
Part of #4184
2022-10-08 11:43:19 -07:00
Daniel Imms 8db2244f5d Merge pull request #4187 from Tyriar/4183_on_naming
Lint rule for on=event emitter and rename all methods with on prefix to handle
2022-10-08 11:14:15 -07:00
Daniel Imms 5d8e4a0af4 Add lint rule to ensure _on prefix is only for event emitters 2022-10-08 11:05:52 -07:00
Daniel Imms eecf52e949 Add lint rule to ensure on prefix is only for event emitters
Fixes #4183
2022-10-08 10:57:31 -07:00
Daniel Imms d8c31d7ad0 Merge pull request #4185 from Tyriar/4167_register_emitter
Fix a bunch of memory retention problems
2022-10-08 10:35:04 -07:00
Daniel Imms d8cf3af555 Fix Marker.dispose 2022-10-08 10:23:19 -07:00
Daniel Imms d3976c05f1 Fix marker dispose unit tests 2022-10-08 09:27:58 -07:00
Daniel Imms 94c3d11239 Fix compile 2022-10-08 09:11:28 -07:00
Daniel Imms 00f6d3a88c Register all event emitters, make some services disposable 2022-10-08 09:00:14 -07:00
Daniel Imms f61d8c91a3 Properly register disposables in SearchAddon 2022-10-08 08:30:39 -07:00
Daniel Imms ac659cb412 Prevent overriding of Disposable.dispose
A common mistake made was overriding dispose and not calling super.dispose,
so a TS trick is now used to move it to a readonly property instead of a
member method which prevents it being overridden.
2022-10-08 08:27:10 -07:00
Daniel Imms 34475f24c3 Release canvas texture atlas 2022-10-08 08:11:28 -07:00
Daniel Imms 8116e035b4 Register webgl renderers 2022-10-08 08:06:43 -07:00
Daniel Imms 56b37a73ac Free WebglAddon 2022-10-08 07:37:55 -07:00
Daniel Imms fc8043a25a Ensure webgl texture atlas is released on dispose 2022-10-08 07:32:51 -07:00
Daniel Imms f88c0e7480 Polish emitter registering in canvas renderer
Part of #4167
2022-10-08 07:24:35 -07:00
Daniel Imms d948b31304 Merge pull request #4181 from Tyriar/clear_race
Fix race condition that could when clearing atlas on 2+ terminals
2022-10-07 16:34:27 -07:00
Daniel Imms 9a4d5dbe26 Merge pull request #4182 from Tyriar/delete_canvas_atlas
Remove the canvas char atlas
2022-10-07 16:32:51 -07:00
Daniel Imms 8e7093f03b Remove the canvas char atlas
This was unused and removing from cache was broken apparently
2022-10-07 16:20:01 -07:00
Daniel ImmsandMegan Rogge c4eef2f470 Fix race condition that could when clearing atlas on 2+ terminals
This is the proper fix for microsoft/vscode#162996, after a bunch of
investigation we found that this is causing the problem:

VS Code clears the texture atlas on OS resume to fix an issue where the texture
could be corrupted. This calls clearTextureAtlas twice which should be fine, but
it's not because inside WebglRenderer.clear we're calling updateModel which is
meant to only ever be called in an animation frame. This calls potentially both
renderers to have stale views of the texture atlas for those rows and our perf
caching of cells (the main purpose of the model) doesn't know it's stale.

Co-Authored-By: Megan Rogge <merogge@microsoft.com>
2022-10-07 16:17:11 -07:00
Daniel Imms 5fcb084089 Merge pull request #4180 from Tyriar/clear_atlas
Fix clearTextureAtlas call as implemented on IRenderer
2022-10-07 14:31:28 -07:00
Daniel Imms e89c507d06 Fix clearTextureAtlas call as implemented on IRenderer
This is the root cause of microsoft/vscode#162996
2022-10-07 14:22:51 -07:00
Daniel Imms eb3de3ebd5 Merge pull request #4151 from Tyriar/renderer_before_open
Allow renderer to be set before Terminal.open is called
2022-10-07 10:17:54 -07:00
Daniel Imms ffa4d921bb Merge remote-tracking branch 'upstream/master' into renderer_before_open 2022-10-07 10:10:24 -07:00
Daniel Imms a060c64a08 Merge pull request #4179 from Tyriar/4178
Split Utf8ToUtf32 decoder unit tests up
2022-10-06 10:38:03 -07:00
Daniel Imms 0d5a526e86 Split Utf8ToUtf32 decoder unit tests up
Fixes #4178
2022-10-06 10:28:54 -07:00
Daniel Imms 3ec08948ec Merge pull request #4177 from Tyriar/tyriar/revert_events
Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
2022-10-06 09:59:50 -07:00
Daniel Imms 4d97729010 Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
This reverts commit d22f7c9edd, reversing
changes made to 1f8e6f0952.
2022-10-06 09:48:54 -07:00
Daniel Imms 5f1651ffbc Merge pull request #4176 from xtermjs/Tyriar-patch-1
Bump copyright year
2022-10-06 09:15:02 -07:00
Daniel Imms ab0082b73a Bump copyright year 2022-10-06 09:14:50 -07:00
Daniel Imms aa55d1bde6 Merge pull request #4170 from Tyriar/shared_cache
Share texture atlas cache code between webgl and canvas renderers
2022-10-05 10:19:17 -07:00
Daniel Imms e0324e32a8 Bring back ImageBitmap optimization in canvas renderer
It's more simple than it used to be, it will clear and debounce all
glyphs when any change is made.
2022-10-04 15:24:10 -07:00
Daniel Imms 3b260a2193 Fix resolved todo 2022-10-04 14:46:00 -07:00
Daniel Imms 622acaaad7 Remove old selection start/end values 2022-10-04 14:45:19 -07:00
Daniel Imms 0d7e18d236 Add CellColorResolver to share load color code 2022-10-04 14:41:27 -07:00
Daniel Imms 83c09c34db Share selection render model code 2022-10-04 14:23:09 -07:00
Daniel Imms 03e3105c55 Fix fg/bg overrides in canvas 2022-10-04 14:10:27 -07:00
Daniel Imms c5d015c908 Merge branch 'master' into shared_cache 2022-10-04 13:54:14 -07:00
Daniel Imms e1e8f28d97 Merge pull request #4118 from coderaiser/feature/lint-by-putout
Lint using 🐊Putout: part 7
2022-10-04 10:45:24 -07:00
Daniel Imms f6a61acc86 Merge branch 'master' into feature/lint-by-putout 2022-10-04 09:52:10 -07:00
Daniel Imms 6316eee672 Comment out debug log 2022-10-02 21:32:24 -07:00
Daniel Imms 9259ec258f Fix combined chars in canvas 2022-10-02 14:27:43 -07:00
Daniel Imms c741c13409 Add ps to output string 2022-10-02 14:25:08 -07:00
Daniel Imms f2ff3a6a7a Add sgr attr test 2022-10-02 14:21:06 -07:00
Daniel Imms dbef850039 Hook up attributes to new texture atlas 2022-10-02 13:57:35 -07:00
Daniel Imms 590a8219e3 Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 13:42:44 -07:00
Daniel Imms d22f7c9edd Merge pull request #4166 from Tyriar/event_with_emitter
Create new event with emitter object to simplify code
2022-10-02 13:41:37 -07:00
Daniel Imms c2bf87ee34 Initial working shared atlas 2022-10-02 11:38:46 -07:00
Daniel Imms 0bc5ca8f80 Fix imports 2022-10-02 11:08:06 -07:00
Daniel Imms 5b785d29a1 Expose _listeners for debug/test 2022-10-02 09:09:05 -07:00
Daniel Imms f38a49b4d5 Move remaining webgl atlas to shared 2022-10-02 08:48:55 -07:00
Daniel Imms 26c974f9b6 Shuffle some utils 2022-10-02 08:37:13 -07:00
Daniel Imms 333b0e28ac Add ITextureAtlas and use that instead of WebglCharAtlas 2022-10-02 07:48:43 -07:00
Daniel Imms 0bc71156bf Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 07:26:23 -07:00
Daniel Imms dece3cc41c DynamicCharAtlas -> CanvasCharAtlas 2022-10-02 07:20:55 -07:00
Daniel Imms 8752c4fa8e Remove canvas char atlas base class 2022-10-02 07:19:57 -07:00
Daniel Imms 53d0297898 Lint client.ts 2022-10-02 07:15:12 -07:00
Daniel Imms af6f03600f Show canvas texture atlas on demo 2022-10-02 07:10:37 -07:00
Daniel Imms 1f8e6f0952 Merge pull request #4169 from Tyriar/canvas_freq
Add willReadFrequently to canvas renderer too
2022-10-01 21:46:13 -07:00
Daniel Imms f087bd121e Add willReadFrequently to canvas renderer too 2022-10-01 21:38:01 -07:00
Daniel Imms d669c6e9d6 Fix remaining references 2022-10-01 18:03:43 -07:00
Daniel Imms c5f9eacbb1 Move other render parts to shared folder 2022-10-01 17:41:16 -07:00
Daniel Imms 6fcc496547 Move cache utils to core shared/ folder
Part of #4065
2022-10-01 17:36:16 -07:00
Daniel Imms 5dee66353a Merge pull request #4168 from Tyriar/4128
Ensure texture atlas comparison uses rgba not object
2022-10-01 10:30:17 -07:00
Daniel Imms 2a602ef231 Ensure texture atlas comparison uses rgba not object
Fixes #4128
2022-10-01 10:20:37 -07:00
Daniel Imms e3e3c347ea Merge pull request #4160 from jerch/tweak_serverside_buffering
tweak buffering in server.js
2022-10-01 09:59:01 -07:00
Daniel Imms 2e8b793ba0 Format files 2022-10-01 08:56:56 -07:00
Daniel Imms 03fcf60aaf Docs 2022-10-01 08:55:01 -07:00
Daniel Imms ed15a8e170 Create new event with emitter object to simplify code 2022-10-01 08:45:40 -07:00
Daniel Imms 969db2bcf9 Merge pull request #4163 from Tyriar/4147
Inline dirty row service into input handler
2022-10-01 08:28:29 -07:00
Daniel Imms 8e378c07e9 Merge pull request #4165 from Tyriar/4164
Define all events and emitters consistently
2022-10-01 08:28:18 -07:00
Daniel Imms 1a22c44f2b Make all emitters readonly 2022-10-01 08:02:32 -07:00
Daniel Imms cf9949840b Use readonly over getter for exposing events
Fixes #4164
2022-10-01 07:52:26 -07:00
Daniel Imms 3a90f7e7c0 Inline dirty row service into input handler
Fixes #4147
2022-10-01 07:39:02 -07:00
Daniel Imms 035025dba3 Merge pull request #4162 from Tyriar/w_refactor
Move w objects to $ prefix variables
2022-10-01 07:33:45 -07:00
Daniel Imms 52ed65d7bd Move w objects to $ prefix variables
Fixes #4138
2022-10-01 07:26:07 -07:00
Jörg Breitbart 150499b7e5 comment fix 2022-09-29 10:46:25 +02:00
Jörg Breitbart 193d305dcd revert timeout to 5ms, remove log in server.js 2022-09-29 10:36:08 +02:00
Jörg Breitbart 2236e230fc Merge branch 'master' into tweak_serverside_buffering 2022-09-28 20:06:01 +02:00
Jörg Breitbart cd6f370c28 unbuffered delivery for userInput response only 2022-09-28 20:03:55 +02:00
Daniel Imms 2659de2291 Merge pull request #4159 from Tyriar/4158
Use microtask to flush the write buffer after input
2022-09-28 10:46:15 -07:00
Daniel Imms 34df17e36d Call innerWrite immediately over a microtask 2022-09-28 09:51:28 -07:00
Daniel Imms 44388e5e6c Use microtask to flush the write buffer after input
Fixes #4158
2022-09-28 09:06:07 -07:00
Jörg Breitbart 785b932d75 lazy cleanup on buffer 2022-09-28 15:57:58 +02:00
Jörg Breitbart e58f73dc82 Merge branch 'master' into buffer_optimizations 2022-09-28 13:22:16 +02:00
Jörg Breitbart 7bba07e24e rename fourByteCells to uint32Cells 2022-09-28 13:18:31 +02:00
Jörg Breitbart 39eea8ed88 remove useless condition 2022-09-28 13:15:34 +02:00
Jörg Breitbart 20c1f1a5f7 fix resize edge cases: always report cleanup state 2022-09-28 12:14:09 +02:00
Jörg Breitbart e87f70d6d6 removed EMPTY_DATA branch 2022-09-28 11:55:26 +02:00
Daniel Imms 51c580e7a4 Merge pull request #4157 from Tyriar/4156
Fix trusted types in dom renderer
2022-09-27 13:15:25 -07:00
Daniel Imms 23328a394b Fix trusted types in dom renderer
Fixes #4156
2022-09-27 13:01:36 -07:00
Daniel Imms 399621eeb5 Merge pull request #4154 from Tyriar/qmt
Remove queuemicrotask polyfill
2022-09-27 07:41:57 -07:00
Daniel Imms 66675ef649 Merge pull request #4153 from Tyriar/microtask_fix
Fix repetitive microtask problem
2022-09-27 06:57:00 -07:00
Daniel Imms c6df208a3e Merge pull request #4155 from Tyriar/fix_priority_queue
Fix priority task queue and idle callback fallback
2022-09-27 06:56:52 -07:00
Daniel Imms 8201925fe8 Fix priority task queue and idle callback fallback
Mistake of not hooking up the abstract methods, this should throw on
Safari for example.
2022-09-27 06:46:19 -07:00
Daniel Imms d74e43a42a Remove queuemicrotask polyfill
Support seems good
2022-09-27 06:44:18 -07:00
Daniel Imms 25b69e5ed0 Fix repetitive microtask problem
See #4145
2022-09-27 06:43:07 -07:00
Daniel Imms 37c722d556 Merge pull request #4152 from dstein64/master
Move real-world uses section down
2022-09-26 08:27:21 -07:00
Daniel Steinberg 2b597a82a1 Move real-world uses section down 2022-09-26 11:19:44 -04:00
Daniel Imms 318abe641b Merge pull request #4145 from Tyriar/microtask_server
Optimize critical I/O path for input latency
2022-09-25 09:39:02 -07:00
Daniel Imms eca9c721d4 Merge pull request #4144 from Tyriar/priority_queue
Add PriorityTaskQueue
2022-09-25 09:38:44 -07:00
Daniel Imms 0b8120808c Prefer microtask to timeout in write queue
This seems to make little difference but it's more correct as we want
to write as fast as possible to minimize input latency
2022-09-24 16:27:56 -07:00
Daniel Imms c6e4788344 Use microtasks on server
This reduces demo input latency on my mac approx 10ms -> 4ms
2022-09-24 16:23:14 -07:00
Daniel Imms c548977a3e Fix imports 2022-09-24 16:16:14 -07:00
Daniel Imms 04d196a32d Use task queue interface 2022-09-24 16:14:38 -07:00
Daniel Imms 494fe3a95d Fill in ITaskQueue 2022-09-24 16:13:53 -07:00
Daniel Imms 8f8813a19a Add PriorityTaskQueue 2022-09-24 15:16:02 -07:00
Daniel Imms df8a57d14b Merge pull request #4143 from Tyriar/idle_deadline
Use the browser's IdleDeadline to determine max task time
2022-09-24 15:00:02 -07:00
Daniel Imms 868fc7ec29 Allow loading canvas before open 2022-09-24 14:44:13 -07:00
Daniel Imms 2febe19222 Set renderer after screen element is attached to dom 2022-09-24 14:42:57 -07:00
Daniel Imms 693d09cbad Allow setting renderer addon before open is called 2022-09-24 14:41:48 -07:00
Daniel Imms ab90dcd0dc Clarify comment 2022-09-24 14:13:04 -07:00
Daniel Imms 1f45d5344c Use the browser's IdleDeadline to determine max task time 2022-09-24 14:10:29 -07:00
Daniel Imms a9eea3fd46 Merge pull request #4142 from Tyriar/defer_paused_resize
Defer paused renderer resize to idle callback
2022-09-24 12:35:40 -07:00
Daniel Imms aec1f2b2be Fix import 2022-09-24 11:58:17 -07:00
Daniel Imms 66efb0388a Move prop to top 2022-09-24 11:57:12 -07:00
Daniel Imms 4ac5a91c95 Make DebouncedIdleTask helper 2022-09-24 11:56:46 -07:00
Daniel Imms 4e8e6b9788 Defer paused renderer resize to idle callback 2022-09-24 11:45:48 -07:00
Daniel Imms 275a8a91ee Merge pull request #4141 from Tyriar/idle_task_queue
Do char atlas warmup via new IdleTaskQueue
2022-09-24 11:44:10 -07:00
Daniel Imms 270ac1c7d9 Docs 2022-09-24 11:17:39 -07:00
Daniel Imms c42f290eb8 Do char atlas warmup via new IdleTaskQueue 2022-09-24 11:10:54 -07:00
Daniel Imms 8b323bbf69 Merge pull request #4140 from Tyriar/fast_setTheme
Share rgba vars throughout Color.ts, fast setTheme parseColor
2022-09-24 11:09:51 -07:00
Daniel Imms e68bf3ca5b Merge remote-tracking branch 'ups/master' into fast_setTheme 2022-09-24 11:02:15 -07:00
Daniel Imms b742da4bfa Merge pull request #4137 from Tyriar/willReadFrequently
Optimize contexts for reading frequently
2022-09-24 10:40:39 -07:00
Daniel Imms 6368cb4cff Move canvas-based parsing to Color.ts lib
This removed the allowTransparency code, it didn't actually do much
before, just whether the 'advanced parsing' would allow transparency.
Since we already do the common formats, I'm not even sure what other
format it could be.
2022-09-24 10:37:40 -07:00
Daniel Imms 81d68b115c Share rgba vars throughout Color.ts, fast setTheme parseColor 2022-09-24 10:00:57 -07:00
Daniel Imms d62b4531c3 Optimize contexts for reading frequently
This was a recommendation from Edge.
Docs here: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
2022-09-24 06:58:01 -07:00
Daniel Imms 1431210ecd Merge pull request #4125 from BattlefieldDuck/master
Update allowProposedApi comments
2022-09-23 13:27:25 -07:00
Daniel Imms 50b1e1045c Merge pull request #4134 from Tyriar/fix_upper_one_eighth_block
Fix width of upper 1/8 block char
2022-09-23 10:33:19 -07:00
Daniel Imms 09a5da5d33 Fix width of upper 1/8 block char
See microsoft/vscode#161323
2022-09-23 10:11:28 -07:00
Daniel Imms 349acbba34 Merge pull request #4131 from Tyriar/idleWarmup
Perform texture atlas warm up in an idle callback
2022-09-22 13:48:56 -07:00
Daniel Imms 78128762f9 Perform texture atlas warm up in an idle callback
Part of #4103
2022-09-22 13:06:08 -07:00
coderaiser 497cfcb74c Merge branch 'master' into feature/lint-by-putout 2022-09-19 12:00:14 +03:00
TatLead 11c806a4d0 Update xterm.d.ts 2022-09-19 05:54:11 +08:00
TatLead 9fb84cb89d Update xterm-headless.d.ts 2022-09-19 05:53:50 +08:00
Daniel Imms 1ceec0b6fa Merge pull request #4123 from Tyriar/remove_v5_publishing
Remove special v5 beta publishing
2022-09-15 13:02:17 -07:00
coderaiser bbff3e8396 Update src/common/services/InstantiationService.ts 2022-09-15 22:52:50 +03:00
Daniel Imms 73cb96ddec Remove special v5 beta publishing 2022-09-15 12:39:32 -07:00
Daniel Imms 904ddffc8d Merge pull request #4122 from Tyriar/release_v5
Release v5, including addon version bumps
2022-09-15 12:33:47 -07:00
Daniel Imms c23f4313fb Release v5, including addon version bumps 2022-09-15 12:22:32 -07:00
coderaiser 25eb7d6472 chore: lint using 🐊Putout 2022-09-15 17:19:34 +03:00
Jörg Breitbart 6225cc2b9c optimize certain resize conditions 2022-09-13 14:37:29 +02:00
Daniel Imms d3fd3d0f22 Merge pull request #4098 from jerch/protected
protection flag support
2022-09-12 05:52:43 -07:00
Daniel Imms 3387d7fe46 Merge pull request #4109 from Tyriar/4108
Copy extended attributes to dest in reflow
2022-09-12 05:50:08 -07:00
jerch 74b5d3f881 Merge branch 'master' into protected 2022-09-09 22:58:29 +02:00
Daniel Imms 822ac527ba Correct type and use ! to highlight the unsafe part 2022-09-09 12:48:55 -07:00
Daniel Imms 2032f409cf Copy extended attributes to dest in reflow
Fixes #4108
2022-09-09 12:28:27 -07:00
Daniel Imms f3622dfedc Merge pull request #4069 from mihaip/master
Make terminal rendering work in popout windows
2022-09-09 10:18:11 -07:00
Mihai Parparita a39a468f19 Make terminal rendering work in popout windows
Add support for the parent element (what is passed to Terminal.open)
being in a different (same origin) window. Accesses to DOM APIs such
as requestAnimationFrame and devicePixelRatio thus need to be scoped
to the corrent window, instead of assuming that it's the same as the
window/global scope where the code is running.

This is done by inferring a parent window at the creation time, and
then storing it in CoreBrowserService, which is already passed to most
places that need it.

To catch future regressions, an ESLint rule that checks for global
accesses is added (it uses AST selectors via the no-restricted-syntax
rule).

This should also be applicable when the parent element is an iframe.

Fixes #3758
2022-09-08 16:59:31 -07:00
Mihai Parparita 2935d9feb7 Add demo of opening the terminal in a popout window
The poput window is just a rendering surface, the termminal code still
executes in the original/parent window.
2022-09-08 15:13:17 -07:00
Jörg Breitbart 21105b4017 Merge remote-tracking branch 'upstream/master' into protected 2022-09-07 19:51:51 +02:00
Jörg Breitbart b01a94f5bb rename to respectProtect 2022-09-07 19:47:28 +02:00
Daniel Imms c57a52b900 Merge pull request #4105 from jeanp413/jp/fix-vs-160070
Fixes https://github.com/microsoft/vscode/issues/160070
2022-09-07 10:00:43 -07:00
Jean Pierre 3953db0d68 Fixes https://github.com/microsoft/vscode/issues/160070 2022-09-07 11:56:52 -05:00
Daniel Imms 1923473a73 Merge pull request #4007 from ink404/handle-delete-composition-helper
Handle Delete in `CompositionHelper`
2022-09-07 09:43:49 -07:00
Daniel Imms d739191adb Merge branch 'master' into handle-delete-composition-helper 2022-09-07 09:39:54 -07:00
Daniel Imms 9ef2397918 Merge pull request #4101 from JasonXJ/render-bug
WebglRenderer always clears _glyphRenderer if _model is cleared
2022-09-07 09:01:41 -07:00
Daniel Imms e9d276a827 Merge pull request #4095 from jerch/request_mode
DECRQM support
2022-09-07 08:58:33 -07:00
Daniel Imms 327ece07e8 Merge pull request #4102 from JasonXJ/clip-cursor-cell
Webgl cursor renderer makes sure a char is not drawn outside of the cell
2022-09-07 07:49:24 -07:00
Jason Lin 4752f99e03 Webgl cursor renderer makes sure a char is not drawn outside of the cell 2022-09-06 14:12:15 +10:00
Jason Lin e5ad3ce9b9 Polish 2022-09-05 17:32:02 +10:00
Jason Lin faf5f72c1f Clear glyph render when the webgl model is cleared 2022-09-05 13:32:04 +10:00
Jörg Breitbart e72e575c0f fix typo in docs 2022-09-04 13:15:47 +02:00
Jörg Breitbart 318b60418e cleanup vtfeaures docs 2022-09-04 12:54:27 +02:00
Jörg Breitbart 68fe100841 cleanup 2022-09-04 11:51:41 +02:00
Jörg Breitbart d511720f63 reshape DECRQSS 2022-09-04 11:34:31 +02:00
Jörg Breitbart 2608db2728 unit tests, fix DECRQSS report 2022-09-04 11:04:07 +02:00
Jörg Breitbart fc46d69a57 add protection flag to DECSED/DECSEL, add DECSCA 2022-09-03 23:52:30 +02:00
jerch 74fc7e2686 Merge branch 'master' into request_mode 2022-09-03 13:32:27 +02:00
Jörg Breitbart f08ad87e7a unit tests 2022-09-03 13:25:20 +02:00
Jörg Breitbart 65f0f9a1d4 restructure code, enable DECSET 12 and SM 20 2022-09-03 12:14:53 +02:00
Daniel Imms 93bc9bf152 Merge pull request #4004 from silamon/requiredoptions3
ITerminalOptions: Make fields required
2022-09-02 13:20:49 -07:00
Daniel Imms e8e2f16cff Merge remote-tracking branch 'upstream/master' into pr/silamon/4004 2022-09-02 13:05:52 -07:00
Daniel Imms 1b38a004a3 Merge pull request #3992 from SvanT/master
Bump minimum target to es2015
2022-09-02 12:43:40 -07:00
Jörg Breitbart df1d72aee5 make linter happy 2022-09-02 01:51:27 +02:00
Jörg Breitbart bb4fbf2659 DECRQM impl 2022-09-02 01:43:31 +02:00
jerch f9db65c52e Merge pull request #4093 from jerch/sgr_pixels
SGR-pixels mouse report
2022-09-01 18:04:50 +02:00
Daniel Imms f816ed0f71 Merge pull request #4094 from sorenisanerd/gotty-maintainer-change
Reference sorenisanerd fork of gotty
2022-09-01 08:26:21 -07:00
Søren L. Hansen 64c8c16730 Reference sorenisanerd fork of gotty
I took over maintainership of gotty a while ago. The original project
is dormant or dead.
2022-09-01 08:22:07 -07:00
Jörg Breitbart 70d30f5a09 fix mouse api tests 2022-09-01 13:31:51 +02:00
Jörg Breitbart edc4fb4a29 document pixels mouse mode 2022-09-01 12:46:10 +02:00
Jörg Breitbart ef1b773fd7 fix pixel to cell offsets 2022-09-01 12:35:44 +02:00
Jörg Breitbart 06d6e08347 sgr_pixels mouse report 2022-09-01 00:50:11 +02:00
Daniel Imms 3acf08bc0c Merge pull request #4090 from xtermjs/Tyriar-patch-1
Create codeql-analysis.yml
2022-08-31 07:39:29 -07:00
Daniel Imms ab8ce61e56 Remove python scanning 2022-08-31 07:26:23 -07:00
Daniel Imms c5a6b997d1 Remove lgtm comments in favor of codeql dismissable alerts 2022-08-31 07:25:03 -07:00
Daniel Imms 6d2d4555e7 Create codeql-analysis.yml 2022-08-31 06:28:58 -07:00
Daniel Imms d673e02afe Merge pull request #4089 from Tyriar/lgtm
Fix lgtm recommendations
2022-08-31 06:26:56 -07:00
Daniel Imms 151a5d88ce Merge pull request #4075 from kenrussell/context-restoration
Handle WebGL context loss/restore events in WebGL renderer.
2022-08-31 05:56:30 -07:00
Daniel Imms f309affff2 Tweak handling of _contextRestorationTimeout 2022-08-31 05:44:48 -07:00
Kenneth Russell 9459553665 Handle WebGL context loss/restore events in WebGL renderer.
If the context is restored within a brief period of time (3 seconds),
recreate members dependent on WebGL state, and fire onRequestRedraw to
the renderer's listeners.

Handle changes to the char atlas better in the demo client.

Tested in Chrome Canary on macOS by running the demo in one window,
and visiting the internal URL about:gpucrash in another window. The
terminal now recovers and redraws properly.
2022-08-30 19:42:22 -07:00
Daniel Imms 4a6e7bee21 Fix lgtm recommendations 2022-08-30 14:09:39 -07:00
Daniel Imms ef759a6d7b Merge pull request #4086 from Tyriar/padding_powerline
Only remove padding from restricted powerline chars
2022-08-30 08:53:03 -07:00
Daniel Imms 9dca3c48c7 Merge pull request #4088 from Tyriar/hyperlink_ternary_fix
Fix ternary in activate call
2022-08-30 07:57:21 -07:00
Daniel Imms 284d9f9893 Fix ternary in activate call
Part of #1134
2022-08-30 07:51:32 -07:00
Daniel Imms 99c47eb3c3 Merge pull request #4087 from Tyriar/hyperlink_range
Expose range on link handler
2022-08-30 07:08:06 -07:00
Daniel Imms c5861d512c Import from Types 2022-08-30 07:01:23 -07:00
Daniel Imms 22cca3495a Expose range on link handler
This is needed for proper tooltip positioning

Part of #1134
2022-08-30 07:00:33 -07:00
Daniel Imms 0c864cddba Merge pull request #4005 from Tyriar/1134_3
OSC hyperlink support
2022-08-30 06:03:36 -07:00
Daniel Imms 3f3fc183f4 Merge branch 'master' into 1134_3 2022-08-30 05:53:36 -07:00
Ian K cad6b49e39 Merge branch 'master' into handle-delete-composition-helper 2022-08-29 21:27:17 -05:00
Daniel Imms 7dae4bc8bc Merge pull request #4085 from Tyriar/fix_gap
Fix rounding error gap in webgl renderer
2022-08-28 13:36:13 -07:00
Daniel Imms 5c6d16ef0a Only remove padding from restricted powerline chars 2022-08-28 13:33:09 -07:00
Daniel Imms f7e0f69598 Fix rounding error gap in webgl renderer
Fixes #4054
Fixes #4076
2022-08-28 13:22:29 -07:00
Daniel Imms 591f178d31 Merge pull request #4084 from Tyriar/more_gc_2
Optimize gc in WebglRenderer and RectangleRenderer render loops
2022-08-28 09:30:55 -07:00
Daniel Imms 3b4ecd0709 Optimize gc in rectangle renderer 2022-08-28 09:20:24 -07:00
Daniel Imms 4a50ca81c6 Optimize gc for WebglRenderer._updateModel and BufferLine.loadCell
RectangleRenderer not done yet
2022-08-28 09:06:42 -07:00
Daniel Imms 4effa381f7 Merge pull request #4081 from Tyriar/more_gc
More garbage collection optimizations for GlyphRenderer
2022-08-28 08:53:48 -07:00
Daniel Imms caaca169eb Add multi key map tests 2022-08-28 08:45:36 -07:00
Daniel Imms 1d945fdffc Create 2 and 4 key map objects, use in color and glyph caches 2022-08-28 08:34:51 -07:00
Daniel Imms 23ee719f6e Remove more garbage collection issues from cell render loop
Part of #4079
2022-08-28 08:02:35 -07:00
Daniel Imms eb31850e87 Merge pull request #4080 from Tyriar/decoration_gc
Optimize gc from decoration-related hot code paths
2022-08-28 07:44:34 -07:00
Daniel Imms 1b64a09f50 Correct conditions after moving to work object 2022-08-27 21:35:56 -07:00
Daniel Imms 823f7345b0 Merge pull request #4078 from Tyriar/fix_demo_resize
Fix resize on demo
2022-08-27 20:52:16 -07:00
Daniel Imms 0727741ca1 Remove iterators from IDecorationService
They're too low performance for typical use cases

Fixes #4079
2022-08-27 20:50:16 -07:00
Daniel Imms 5e3f63e876 Avoid garbage collection in webgl decoration hot code
Part of #4079
2022-08-27 20:06:58 -07:00
Daniel Imms 0237b2965e Fix resize on demo 2022-08-27 19:28:20 -07:00
Daniel Imms 3105797da1 Merge pull request #4077 from Tyriar/4063
Take char width into account when drawing underlines/strokethrough
2022-08-27 07:02:33 -07:00
Daniel Imms 4d837e2802 Add an emoji to underline test 2022-08-27 06:39:07 -07:00
Daniel Imms 4ecc098de4 Get width of emoji/combined chars 2022-08-27 06:38:15 -07:00
Daniel Imms e76918eddc Take char width into account when drawing underlines/strokethrough
Fixes #4063
2022-08-27 06:34:38 -07:00
Daniel Imms 903830ce1f Add CJK chars to underline test 2022-08-27 06:06:17 -07:00
Daniel Imms 2526409ca5 Merge pull request #4074 from Tyriar/4059
Only draw character outline if it has a descent
2022-08-26 14:38:40 -07:00
Daniel Imms 7ffd9e1f7f Only draw character outline if it has a descent
Fixes #4059
2022-08-26 14:16:02 -07:00
Daniel Imms 4979e9d2be Show test characters in underline test 2022-08-26 13:26:00 -07:00
Daniel Imms 8bbbc98ac8 Merge pull request #4073 from Tyriar/4072
Show entire glyph for extra powerline symbols
2022-08-26 08:33:53 -07:00
Daniel Imms 98ceafb57a Merge pull request #4070 from Tyriar/4066
Improve glyph padding, especially in large font sizes
2022-08-26 08:15:57 -07:00
Daniel Imms f6860442fb Show entire glyph for extra powerline symbols
Fixes #4072
2022-08-26 08:12:00 -07:00
Daniel Imms 4382a551d1 Use powerline extra symbols in demo font family 2022-08-26 07:53:20 -07:00
Daniel Imms 774ebe40e6 Improve glyph padding, especially in large font sizes
Fixes #4066
2022-08-26 07:26:36 -07:00
Ian K 6e3388a2c4 Merge branch 'master' into handle-delete-composition-helper 2022-08-26 09:09:02 -05:00
Daniel Imms d92d7904ab Merge pull request #4068 from Tyriar/vscode_158497
Properly clear underscore glyph's background
2022-08-24 15:43:42 -07:00
Daniel Imms 523de80162 Merge branch 'master' into vscode_158497 2022-08-24 15:36:44 -07:00
Daniel Imms e40cf0684e Properly clear underscore glyph's background
See microsoft/vscode#158497
2022-08-24 15:36:03 -07:00
Daniel Imms b68f82078b Merge pull request #4055 from GreenMashimaro/fix-typo
Fix typo and remove unnecessary eslint comments
2022-08-24 08:55:41 -07:00
Daniel Imms 86ba6cd288 Tweak comment 2022-08-24 08:55:11 -07:00
greenmashimaro ac81d3f72d feat: restore LocalizableStrings.ts const export 2022-08-24 22:43:00 +08:00
greenmashimaro 559b6950b2 remove unnecessary eslint comments 2022-08-23 15:09:52 +08:00
greenmashimaro b59de22c36 fix typo 2022-08-23 15:09:43 +08:00
Ian K 86c8ff0e33 Merge branch 'master' into handle-delete-composition-helper 2022-08-21 09:26:45 -05:00
Daniel Imms c0ef2ccb5a Merge pull request #4053 from Tyriar/underline_polish
Polish underline for more fonts to prevent odd overlap look
2022-08-19 13:54:41 -07:00
Daniel Imms 0698afaf12 Only stroke glyph when font size >= 12 2022-08-19 13:24:39 -07:00
Daniel Imms 79d47893df Move webgl underline rendering down
See microsoft/vscode#158326
2022-08-19 13:19:08 -07:00
Daniel Imms 7e6282e9c2 Merge pull request #4010 from xtermjs/Tyriar-patch-1
Whitespace change to trigger release
2022-08-10 06:27:44 -07:00
Daniel Imms 9a119a7292 Whitespace change to trigger release 2022-08-10 06:27:32 -07:00
Daniel Imms 45b019bd89 Merge pull request #4009 from Tyriar/vscode157444
Ensure canvas device dims are > 0x0 when firing callback
2022-08-10 06:02:49 -07:00
Daniel Imms 81a62306af Ensure canvas device dims are > 0x0 when firing callback
See microsoft/vscode#157444
2022-08-10 05:59:29 -07:00
Ian K 5312602692 handle same length case 2022-08-08 17:02:21 -05:00
Ian K 413a885b5e handle if the newValue.length is less than the old value by deleting 2022-08-08 12:41:12 -05:00
Daniel Imms bc528cd870 Ensure extended underline style is cleared on SGR 24 2022-08-07 11:51:52 -07:00
Daniel Imms 2cfbee413f Add some OscLinkService unit tests 2022-08-07 11:30:08 -07:00
Daniel Imms e87e15e87b Only override AttributeData.isUnderline when urlId is set 2022-08-07 10:36:42 -07:00
Daniel Imms 8191483fbd Evict links from maps when markers are disposed 2022-08-07 08:05:42 -07:00
Daniel Imms 9c9ff2b38c Clean up 2022-08-07 07:46:16 -07:00
Daniel Imms c39d235168 Fix adjacent link edge case 2022-08-07 07:42:21 -07:00
Daniel Imms d9b8c6a838 linkHandler option and default handler 2022-08-07 07:35:46 -07:00
Daniel Imms ace99d125d Progress on osc link service 2022-08-07 07:02:42 -07:00
Daniel Imms fd79100d95 Fix test service injection 2022-08-07 06:10:27 -07:00
Daniel Imms 7970b5c1e3 Fix edge cases in link creation 2022-08-07 06:05:42 -07:00
Daniel Imms 91d46dae50 OSC link progress 2022-08-07 05:08:13 -07:00
Simon Lamon 6e123e90ee Add example back 2022-08-05 16:56:54 +00:00
Simon Lamon a1385f589b Required options 2022-08-05 16:16:12 +00:00
Svante Boberg 4f46f8e123 Merge branch 'xtermjs:master' into master 2022-08-04 22:05:07 +02:00
Daniel Imms c021571194 Merge pull request #4003 from Tyriar/liga_no_node
Remove node target from ligatures
2022-08-04 10:31:02 -07:00
Daniel Imms f0926e7821 Remove tests that rely on node
We can test this primarily with api tests instead
2022-08-04 10:11:11 -07:00
Daniel Imms d495fdd9ab Bring back webpack resolve config 2022-08-04 10:01:58 -07:00
Daniel Imms f3f6a91f16 Remove node target from ligatures
This complicates bundling for sandboxed electron a lot, ideally we would only
rely on browser APIs for font access
2022-08-04 09:57:37 -07:00
Daniel Imms 409cb4bb73 Merge pull request #4002 from Tyriar/liga_target
Remove electron renderer target from ligatures addon
2022-08-04 06:29:41 -07:00
Daniel Imms d608912efd Remove electron renderer target
This causes global to be required which is not available in sandboxed electron
renderers

See microsoft/vscode#157008
2022-08-04 06:20:45 -07:00
Daniel Imms 9d7cef3d63 Merge pull request #3991 from deepanshu44/ansi-colors
print colors
2022-08-04 06:08:40 -07:00
Daniel Imms bc57ea3a7a Reduce duplicate, improve layout 2022-08-04 06:06:31 -07:00
Daniel Imms daba61bd35 Merge pull request #3999 from Tyriar/peer_dep
Bump all peer deps to v5
2022-08-04 05:04:09 -07:00
Daniel Imms 76d9d5a324 Bump all peer deps to v5
Fixes #3997
2022-08-03 09:45:23 -07:00
Daniel Imms 830f8f3231 Merge pull request #3983 from silamon/fixdemo
Fix demo
2022-08-03 04:26:02 -07:00
Simon Lamon 04eb6a4160 Fix wrong merge conflict resolution 2022-08-01 18:00:23 +02:00
Simon Lamon 3f1480a52a Merge branch 'master' into fixdemo 2022-08-01 17:56:30 +02:00
Svante Boberg 14b02b9ad5 Bump minimum target to es2015
Now that we no longer support IE we can raise the minimum ES target a
bit. Changing to es2015 shaves off ~17% from the main bundle size, also
it prevents generators from being transpiled into regular functions etc.
so I would guess it has the potential to give some general performance
improvements too.

For fun I also tried to raise it to es2022, this gave a slightly bigger
bundle than es2015 so I think es2015 could be an ok target unless we
need something newer for some reason.
2022-08-01 10:17:10 +02:00
deepanshu f3cd6134f1 print colors 2022-08-01 11:29:12 +05:30
Daniel Imms 9261780a1d Merge pull request #3989 from Tyriar/3970_2
Don't dim default bg in webgl
2022-07-31 16:28:15 -07:00
Daniel Imms a434800303 Merge pull request #3988 from Tyriar/fix_remove_atlas
Fix unchecking webgl in demo
2022-07-31 15:42:44 -07:00
Daniel Imms 39959c2861 Don't dim default bg in webgl
Fixes #3970
2022-07-31 15:39:34 -07:00
Daniel Imms 03c75360b1 Fix unchecking webgl in demo 2022-07-31 15:28:27 -07:00
Daniel Imms 85984681bb Merge pull request #3987 from Tyriar/3986
Polish powerline rendering
2022-07-31 14:39:54 -07:00
Daniel Imms 0910e58b15 Merge pull request #3924 from silamon/allowmarkersonaltbuffer
Allow markers on the alt buffer
2022-07-31 14:39:45 -07:00
Daniel Imms 14d59999e4 Update src/common/buffer/BufferSet.test.ts 2022-07-31 14:39:37 -07:00
Daniel Imms 8f6bde9641 Polish powerline rendering
Fixes #3986
2022-07-31 14:27:07 -07:00
Daniel Imms 436f8556fd Merge pull request #3972 from silamon/rowscolsheadless
Apply row/col changes from xterm to xterm-headless
2022-07-31 12:43:47 -07:00
Daniel Imms face97d012 Merge pull request #3985 from Tyriar/3969
Fix powerlines, custom glyph offset and improve demo
2022-07-31 12:39:18 -07:00
Daniel Imms 8d92f91029 Always update terminal size in demo 2022-07-31 12:19:39 -07:00
Daniel Imms 1ede106ab4 Fix powerlines not clipped when bg changes 2022-07-31 12:05:20 -07:00
Daniel Imms e7e71dbd3c Merge remote-tracking branch 'upstream/master' into 3969 2022-07-31 11:35:47 -07:00
Daniel Imms 208364147a Allow texture atlas to be zoomed on hover 2022-07-31 11:35:12 -07:00
Daniel Imms d6fe5c7384 Update size when letter spacing changes 2022-07-31 11:21:27 -07:00
Daniel Imms 75ca5006a2 Expose texture atlas change event and use in demo 2022-07-31 11:16:12 -07:00
Daniel Imms 266d795179 Fix line height and letter spacing for custom glyphs 2022-07-31 10:58:04 -07:00
Simon Lamon d0571d8634 Fix formatting 2022-07-31 08:59:25 +00:00
Simon Lamon ca10ef5cad Fix demo 2022-07-31 08:55:44 +00:00
Simon Lamon 31686fda61 Fix tests 2022-07-31 07:54:29 +00:00
Simon Lamon 0a83040ac8 clearAllMarkers 2022-07-31 07:38:14 +00:00
Daniel Imms 889e3ad14b Merge pull request #3980 from Tyriar/3971
Fix underline color and polish width and positioning
2022-07-30 23:50:20 -07:00
Daniel Imms 8a596b220a Merge pull request #3981 from Tyriar/3978
Include purpose of canvas addon in readme
2022-07-30 23:49:17 -07:00
Daniel Imms 8d57c64d29 Include purpose of canvas addon in readme
Fixes #3978
2022-07-30 23:48:37 -07:00
Daniel Imms bbd03fb5fe Merge pull request #3976 from Tyriar/clip_underline
Clip outline drawing near underline
2022-07-30 23:43:42 -07:00
Daniel Imms bff529e1e2 Simplify dom tests and fix assertion 2022-07-30 23:41:08 -07:00
Daniel Imms f4618b229f Bring underlines closer to characters 2022-07-30 23:30:52 -07:00
Daniel Imms 686105942c Reduce line width of webgl underline 2022-07-30 23:25:18 -07:00
Daniel Imms 0514b55936 Fix default underline color
Fixes #3971
2022-07-30 23:16:47 -07:00
Daniel Imms 04a14a748e Merge pull request #3979 from Tyriar/3977
Implement improved texture packing strategy
2022-07-30 22:58:12 -07:00
Daniel Imms ed492f7253 Implement improved texture packing strategy
Fixes #3977
2022-07-30 19:24:09 -07:00
Daniel Imms d459e520d3 Merge pull request #3973 from Tyriar/3970
Fix dim flag applying to selection in webgl
2022-07-30 17:58:41 -07:00
Daniel Imms ce4a85d07d Clip outline drawing near underline
Fixes #3975
2022-07-30 17:52:00 -07:00
Daniel Imms 309487a020 Fix character offset to respect texture padding in webgl
Fixes #3974
2022-07-30 11:07:33 -07:00
Daniel Imms c7122e8447 Fix dim flag applying to selection in webgl
Fixes #3970
2022-07-30 10:29:41 -07:00
Simon Lamon 027fffbadc Fix comment 2022-07-30 15:48:49 +00:00
Simon Lamon dc30a51017 Apply row/col changes from xterm to xtmer-headless 2022-07-30 15:46:07 +00:00
Daniel Imms 640596b043 Merge pull request #3921 from Tyriar/underline
Underline style and color rendering
2022-07-30 07:46:56 -07:00
Daniel Imms d3b3357f3f Merge pull request #3966 from silamon/mochaunittestback
Fix mocha test explorer
2022-07-30 07:39:10 -07:00
Daniel Imms f0ddaf49ea Disable threshold check for glyphs with colored underlines
This causes some underlines to disappear which looks very bad for
colored underlines
2022-07-30 07:36:48 -07:00
Simon Lamon d80a47f539 Add functionality back to mocha test explorer 2022-07-30 14:27:29 +00:00
Daniel Imms d0ec9f2591 Support letter spacing in webgl 2022-07-30 07:05:05 -07:00
Daniel Imms 941cc5100d Increase curly height when dpr <= 1 2022-07-30 06:56:32 -07:00
Daniel Imms e0d4384d3c Merge branch 'master' into underline 2022-07-30 06:40:41 -07:00
Simon Lamon f45eb02394 Keep the root directory clean 2022-07-30 13:35:56 +00:00
Daniel Imms db79efdbc1 Merge pull request #3967 from Tyriar/3962
Include version of Windows to disable windowsMode from
2022-07-30 06:27:27 -07:00
Daniel Imms 8afa714276 Include version of Windows to disable windowsMode from
Fixes #3962
2022-07-30 06:27:02 -07:00
Daniel Imms 0befb9c251 Merge pull request #3965 from Tyriar/inactiveSelection
Inactive selection background support
2022-07-30 06:05:57 -07:00
Simon Lamon a8f63c478c Introduce mocharc again 2022-07-30 12:59:08 +00:00
Daniel Imms ff33834eb2 Merge branch 'master' into inactiveSelection 2022-07-29 14:51:22 -07:00
Daniel Imms a2e07de90c Merge pull request #3963 from Tyriar/fallback_ligatures
Support fallback ligatures and correctly dispose of char joiner
2022-07-29 14:51:00 -07:00
Daniel Imms b0c29cac9a Merge pull request #3932 from Tyriar/tyriar/dim_bg
Fix inverse dim rendering on canvas+webgl
2022-07-29 14:50:47 -07:00
Daniel Imms 7f58c48503 Fix tests as errors are no longer thrown on font access fail 2022-07-29 11:27:23 -07:00
Daniel Imms eb93b02c6c Use debug log level to avoid polluting embedder logs 2022-07-29 11:24:23 -07:00
Daniel Imms 11affae761 Merge branch 'master' into inactiveSelection 2022-07-29 11:21:15 -07:00
Daniel Imms 53e4e9ad72 Inactive selection bg in canvas 2022-07-29 11:20:32 -07:00
Daniel Imms 4ac8e4ca51 Remove reliance on core in some webgl parts 2022-07-29 11:09:19 -07:00
Daniel Imms 954d9371a1 Inactive selection in webgl 2022-07-29 11:05:41 -07:00
Daniel Imms 6493679b2e Inactive selection bg in dom renderer
Part of #3803
2022-07-29 11:00:36 -07:00
Daniel Imms 1af6175e48 Merge pull request #3964 from Tyriar/selection_inconsistency
Rename ITheme.selection to selectionBackground
2022-07-29 11:00:11 -07:00
Daniel Imms 906a6fc558 Rename test selection -> selectionBackground 2022-07-29 10:35:48 -07:00
Daniel Imms 9cb25fbf06 Rename ITheme.selection to selectionBackground
This makes it consistent with selectionForeground and other theme keys and
as a result will make the API easier to work with, especially when inactive
selection is done (#3803)
2022-07-29 10:34:27 -07:00
Daniel Imms 48142eca23 Merge branch 'master' into tyriar/dim_bg 2022-07-29 10:20:22 -07:00
Daniel Imms 1fe0ec8d2c Merge branch 'master' into underline 2022-07-29 10:19:52 -07:00
Daniel Imms f8a9eb1db6 Support fallback ligatures and correctly dispose of char joiner 2022-07-29 10:18:21 -07:00
Daniel Imms df14ce49d7 Merge pull request #3936 from Tyriar/ligatures
Get ligatures addon working with latest version of Chrome
2022-07-29 07:33:49 -07:00
Daniel Imms 7f5ed40db1 Merge pull request #3960 from Tyriar/init_options
Make cols/rows only usable in the ctor
2022-07-29 07:20:51 -07:00
Daniel Imms 29aca47e0d Merge pull request #3961 from Tyriar/canvas_fix2
Remove instantiation service from canvas addon completely
2022-07-28 16:50:29 -07:00
Daniel Imms 055bcce18d Remove instantiation service from canvas addon completely 2022-07-28 11:31:51 -07:00
Daniel Imms 006249ce65 Make cols/rows only usable in the ctor
Fixes #3825
2022-07-28 11:16:12 -07:00
Daniel Imms 60f9bf4398 Merge pull request #3959 from Tyriar/canvas_services
Fix service access in canvas addon
2022-07-28 11:11:08 -07:00
Daniel Imms 848aceaa25 Merge branch 'master' into allowmarkersonaltbuffer 2022-07-28 11:05:24 -07:00
Daniel Imms 9e650cea9f Fix service access in canvas addon 2022-07-28 11:00:08 -07:00
Daniel Imms de78344165 Merge pull request #3919 from SvanT/master
Dispose events
2022-07-28 10:52:09 -07:00
Daniel Imms 2862832f6f Merge pull request #3958 from Tyriar/dispose_doc
Clarify you shouldn't use the object after Terminal.dispose
2022-07-28 10:51:21 -07:00
Daniel Imms 8df878b8a9 Clarify you shouldn't use the object after Terminal.dispose
Fixes #3939
2022-07-28 10:46:12 -07:00
Daniel Imms 0e07e419a2 Merge branch 'master' into allowmarkersonaltbuffer 2022-07-28 10:26:12 -07:00
Daniel Imms 04b513cf9b Merge pull request #3926 from Tyriar/rounding
FINALLY fix blurry canvas/webgl renderer issues due to device pixel rounding errors 🎉
2022-07-28 10:25:01 -07:00
Daniel Imms f310f118b4 Merge pull request #3957 from Tyriar/publish_addon
Fix publishing addons as xterm
2022-07-28 10:21:39 -07:00
Daniel Imms 8d308a7309 Merge branch 'master' into publish_addon 2022-07-28 10:21:26 -07:00
Daniel Imms d9880c0270 Fix publishing addons as xterm 2022-07-28 10:20:57 -07:00
Daniel Imms f6940aa201 Merge pull request #3940 from Tyriar/1140
Implement duration-based smooth scrolling
2022-07-28 09:27:49 -07:00
Daniel Imms 2482c0e8d3 Merge pull request #3956 from Tyriar/fix_addon_version
Fix version of published addons
2022-07-28 09:13:37 -07:00
Daniel Imms 1be14e4338 Fix version of published addons 2022-07-28 09:09:44 -07:00
Daniel Imms 51f89bbe4d Merge pull request #3954 from Tyriar/v0_1_0
xterm-addon-canvas@0.1.0
2022-07-28 07:30:03 -07:00
Daniel Imms 0b0662c4d9 xterm-addon-canvas@0.1.0 2022-07-28 07:27:27 -07:00
Daniel Imms 9cbd9a0456 Merge pull request #3931 from Tyriar/3930
Never clear color when allowTransparency is on
2022-07-28 07:19:58 -07:00
Daniel Imms 770de8d05b Merge pull request #3929 from Tyriar/3845
Retain hue when flipping luminance
2022-07-28 07:19:38 -07:00
Daniel Imms 4372d8ccca Merge pull request #3933 from Tyriar/3784
Warn when ligature fonts fail to download
2022-07-28 07:18:52 -07:00
Daniel Imms 0b81af1811 Merge pull request #3935 from Tyriar/3759
Remove shift+backspace -> ^H binding
2022-07-28 07:18:30 -07:00
Daniel Imms ed92cc0c3f Merge pull request #3938 from Tyriar/1963
Redraw canvas renderer selection on resize
2022-07-28 07:18:20 -07:00
Daniel Imms 8b276c1a54 Merge remote-tracking branch 'upstream/master' into pr/SvanT/3919 2022-07-28 07:04:11 -07:00
Daniel Imms d2be5f992a Merge remote-tracking branch 'upstream/master' into underline 2022-07-28 07:01:41 -07:00
Daniel Imms 437c8aac5c Merge remote-tracking branch 'upstream/master' into rounding 2022-07-28 07:00:13 -07:00
Daniel Imms 84b259dfa5 Merge remote-tracking branch 'upstream/master' into tyriar/dim_bg 2022-07-28 06:57:27 -07:00
Daniel Imms a57b69d5de Merge remote-tracking branch 'upstream/master' into 1963 2022-07-28 06:56:36 -07:00
Daniel Imms 8f0f33fd6d Merge branch 'master' into 1140 2022-07-28 06:50:46 -07:00
Daniel Imms 4d542d23c6 Merge branch 'master' into ligatures 2022-07-28 06:50:40 -07:00
Daniel Imms 4637983888 Merge branch 'master' into 3759 2022-07-28 06:50:35 -07:00
Daniel Imms f1ff42410e Merge branch 'master' into 3784 2022-07-28 06:50:32 -07:00
Daniel Imms 1ef9f7c1d1 Merge branch 'master' into 3930 2022-07-28 06:50:26 -07:00
Daniel Imms 8901740113 Merge branch 'master' into 3845 2022-07-28 06:50:23 -07:00
Daniel Imms 2fffe9fc89 Merge branch 'master' into allowmarkersonaltbuffer 2022-07-28 06:50:17 -07:00
Daniel Imms 423d5402f7 Merge pull request #3917 from xtermjs/Tyriar-patch-1
Remove note about IE11 which is EOL
2022-07-28 06:50:02 -07:00
Daniel Imms 2b2d6c5fb3 Merge pull request #3943 from xtermjs/v5
v5 breaking changes
2022-07-28 06:46:12 -07:00
Daniel Imms c55075b666 Merge pull request #3953 from Tyriar/v5_publish_stable
Hardcode release step to publish v5.0.0 beta
2022-07-28 06:32:47 -07:00
Daniel Imms 09df783959 Merge pull request #3952 from Tyriar/v5_buffer_range
Replace ISelectionPosition with IBufferRange
2022-07-28 06:32:13 -07:00
Daniel Imms 3bdbfb6d5f Hardcode release step to publish v5.0.0 beta 2022-07-28 06:31:52 -07:00
Daniel Imms 520a270afc Update tests to use new getSelectionPosition API 2022-07-28 06:17:44 -07:00
Daniel Imms d1f53e2b02 Replace ISelectionPosition with IBufferRange
Fixes #2480
2022-07-28 05:58:15 -07:00
Daniel Imms 940c93e9cd Merge pull request #3951 from Tyriar/v5_api
Stabilize buffer and parser APIs
2022-07-28 05:33:15 -07:00
Daniel Imms 1a5c881c7c Stabilize buffer and parser APIs 2022-07-28 05:28:28 -07:00
Daniel Imms fb3886a9ae Merge pull request #3950 from Tyriar/v5_publish_canvas
Publish canvas addon in CI release step
2022-07-28 05:09:26 -07:00
Daniel Imms 3271b45681 Publish canvas addon in CI release step
Part of #3271
2022-07-28 05:08:46 -07:00
Daniel Imms 43f38cddb1 Merge pull request #3949 from Tyriar/v5_canvas
Move the canvas renderer into an addon
2022-07-28 05:07:31 -07:00
Daniel Imms e5037a6d1e Fix fit addon api tests on linux+firefox 2022-07-28 05:00:10 -07:00
Daniel Imms 119b64181b Fix fit addon api tests 2022-07-28 04:51:49 -07:00
Daniel Imms c52eb8898f Simplify addon tsconfigs 2022-07-28 04:50:40 -07:00
Daniel Imms ced5c304f1 Move CustomGlyphs.ts into core 2022-07-28 04:46:18 -07:00
Daniel Imms b3cffe2d30 Fix fit addon api tests on linux 2022-07-28 04:43:03 -07:00
Daniel Imms 73b3e31b38 Canvas readme 2022-07-28 04:37:42 -07:00
Daniel Imms cbddd2029e Get canvas renderer addon working 2022-07-27 16:04:24 -07:00
Daniel Imms 48cfd23219 Fix tests that depended on canvas renderer 2022-07-27 10:33:10 -07:00
Daniel Imms 1c6ce99419 Move canvas renderer contents into addon 2022-07-27 10:26:02 -07:00
Daniel Imms ab7da06cc1 Stub canvas addon 2022-07-27 09:26:52 -07:00
Daniel Imms 440ab765c4 Remove rendererType setting 2022-07-27 09:22:05 -07:00
Daniel Imms e7dbdb1362 Merge pull request #3947 from Tyriar/v5_deprecated
Remove remaining deprecated APIs for v5 (getOption, setOption, writeUtf8)
2022-07-27 09:13:15 -07:00
Daniel Imms 8c16d9d506 Remove writeUtf8 deprecated API 2022-07-27 09:04:43 -07:00
Daniel Imms 71911b501b Remove set/getOption deprecated APIs 2022-07-27 09:04:07 -07:00
Daniel Imms b2aa100c58 Merge pull request #3946 from Tyriar/v5_remove_add_apis
Remove add* APIs and remaining link matcher parts
2022-07-27 08:58:32 -07:00
Daniel Imms 6d932ec812 Fix addMarker in api tests 2022-07-27 08:49:07 -07:00
Daniel Imms 5a658ebfcc Remove remaining link hover tooltip duration 2022-07-27 08:45:32 -07:00
Daniel Imms 7bd3801f58 Remove mouse zone manager
Part of #2703
2022-07-27 08:41:27 -07:00
Daniel Imms 8cfa7a0ee2 Remove deprecated add* APIs
Fixes #2652
2022-07-27 08:35:40 -07:00
Daniel Imms 140073675c Merge pull request #3945 from Tyriar/v5_proposed_api
Set allowProposedApi default to false
2022-07-27 08:29:08 -07:00
Daniel Imms 238a2bafb1 Set allowProposedApi default to false
Fixes #2826
2022-07-27 08:20:54 -07:00
Daniel Imms 09672feca0 Merge pull request #3944 from Tyriar/v5_link_matcher
Remove link matchers
2022-07-27 06:57:30 -07:00
Daniel Imms 7a17a26e18 Remove link matchers
Fixes #2703
2022-07-27 06:49:35 -07:00
Daniel Imms fafa7efd5e Merge pull request #3941 from Tyriar/v5_bell
Remove bell settings and sound service in favor of Terminal.onBell
2022-07-27 06:29:34 -07:00
Daniel Imms 841cd636ca Merge remote-tracking branch 'upstream/v5' into v5_bell 2022-07-27 06:18:48 -07:00
Daniel Imms ceeb8585d7 Merge pull request #3942 from Tyriar/v5_ci
Enable CI on v5 branch
2022-07-27 06:17:19 -07:00
Daniel Imms dc4940e26e Try different syntax 2022-07-27 06:13:28 -07:00
Daniel Imms 8a19f8c1cc Enable CI on v5 branch 2022-07-27 06:04:53 -07:00
Daniel Imms 66d6ed65ef Remove all bellSound/bellStyle/etc. references
Fixes #3316
2022-07-27 05:58:26 -07:00
Daniel Imms 09ae3a9b44 Remove sound service and simplify bell 2022-07-27 05:55:00 -07:00
Daniel Imms 78bfe0b89d Revert accidental setting change 2022-07-27 05:47:39 -07:00
Daniel Imms 4b8e7a5768 Change default to 0 2022-07-27 05:45:56 -07:00
Daniel Imms fb4cb0f140 Reduce smooth scroll GC 2022-07-27 05:44:34 -07:00
Daniel Imms a55728dd28 Add duration-based smooth scroll
Fixes #1140
2022-07-27 05:41:00 -07:00
Svante Boberg 52dc6a456c Remove redundant disposal 2022-07-27 14:34:00 +02:00
Svante Boberg 078b83fcaf Use disposal registration 2022-07-27 14:24:42 +02:00
Svante Boberg c895f67fd8 Add missing webgl cursor blink disposal
Same disposal code as the non-webgl cursor blink layer
2022-07-27 00:00:41 +02:00
Daniel Imms 9159fc68e1 Redraw canvas renderer selection on resize
Fixes #1963
2022-07-25 09:44:54 -07:00
Daniel Imms 5664ff3101 Fix ligature tests in node 2022-07-25 07:42:38 -07:00
Daniel Imms 78ddb4b14a Add link to font access api change 2022-07-25 07:00:58 -07:00
Daniel Imms 4744ef34b2 Remove unneeded comment 2022-07-24 20:44:06 -07:00
Daniel Imms f4bc9f1ecd Pull fonts using latest font access API in Chrome
Part of #958
2022-07-24 19:47:24 -07:00
Daniel Imms b11217aa36 Merge pull request #3920 from Tyriar/task_improvement
Allow start task to be restarted without killing watch
2022-07-24 17:52:41 -07:00
Daniel Imms 34fe6e2260 Merge pull request #3922 from Tyriar/theme_picker
Add simple theme picker to demo
2022-07-24 17:52:28 -07:00
Daniel Imms 160e9ee35b Merge pull request #3923 from Tyriar/atlas_bottom
Don't overlap the atlas ontop of options
2022-07-24 17:52:16 -07:00
Daniel Imms d98198e1b9 Merge pull request #3927 from Tyriar/overviewRulerWidth
Support overviewRulerWidth in demo
2022-07-24 17:52:07 -07:00
Daniel Imms 493975b41e Merge pull request #3934 from Tyriar/3777
Fix test-api running unit tests as well
2022-07-24 17:51:57 -07:00
Daniel Imms 3322418728 Remove shift+backspace -> ^H binding
This has been there since the first commit of term.js, it's not clear
why it's there but Terminal.app, iTerm2 and kitty seem to just do a
regular backspace

Fixes #3759
2022-07-24 17:45:14 -07:00
Daniel Imms 2ceec34809 Remove log 2022-07-24 17:20:41 -07:00
Daniel Imms 8c55c83e4a Fix test-api running unit tests as well
This file was added to support the mocha test explorer in vscode
but it turns out it causes all calls to mocha to run the unit
tests. I couldn't figure out how to ignore through args.

Fixes #3777
2022-07-24 17:12:23 -07:00
Daniel Imms 9acc36f8c0 Warn when ligature fonts fail to download
Fixes #3784
2022-07-24 16:52:11 -07:00
Daniel Imms a4f7e2d8e9 Fix inverse dim on canvas renderer 2022-07-24 13:20:04 -07:00
Daniel Imms ee2e54487d Fix inverse dim for webgl
Fixes #3534
2022-07-24 13:13:26 -07:00
Daniel Imms f4ffb13ece Render dim bg in all renderers
Fixes #3534
2022-07-24 10:59:24 -07:00
Daniel Imms a39c7fb636 Never clear color when allowTransparency is on
It's redundant and wasn't working as expected anyway.

Fixes #3930
2022-07-24 08:31:12 -07:00
Daniel Imms 0ccf3beec7 Retain hue when flipping luminance
Fixes #3845
2022-07-24 08:03:27 -07:00
Daniel Imms 90b6f058c9 Support overviewRulerWidth in demo
Fixes #3771
2022-07-24 07:38:48 -07:00
Daniel Imms 0165c3612d Fix canvas device pixel dimension setting 2022-07-24 07:11:56 -07:00
Daniel Imms 47b1fdd147 Refactor to share observeDevicePixelDimensions 2022-07-24 04:09:34 -07:00
Daniel Imms ad8404417e ts-loader update, correct css cell dimension error 2022-07-24 03:57:22 -07:00
Daniel Imms 3d6c25bf2d typescript@4.7
Required for devicePixelContentBoxSize ResizeObserver API
2022-07-24 03:43:28 -07:00
Daniel Imms e25471361f Fix device pixel value in canvas renderer 2022-07-24 03:41:25 -07:00
Daniel Imms cb10aadb06 Fix device pixel rounding errors in webgl renderer
This uses the ResizeObserver devicePixelContentBoxSize API in order to
fetch the exact device pixel dimensions from the browser. The old
possibly blurry behavior is used as a fallback if that API is not
available.

Part of #2662
Part of microsoft/vscode#85154
2022-07-24 03:31:05 -07:00
Simon Lamon 89f5c2e06c Allow markers on the alt buffer 2022-07-24 09:30:13 +00:00
Daniel Imms dbe2b98bb4 Don't overlap the atlas ontop of options
This would prevent mouse input in some options in narrow screens
2022-07-23 18:57:16 -07:00
Daniel Imms 4087284822 Fix ext caching issues 2022-07-23 15:19:17 -07:00
Daniel Imms 759a5928a2 Continuous curly underline 2022-07-23 14:48:26 -07:00
Daniel Imms 43bbea8eb5 Set webgl as default demo renderer 2022-07-23 14:43:14 -07:00
Daniel Imms fd8fc18302 Clear stroke around text in underline 2022-07-23 14:28:15 -07:00
Daniel Imms f34a30b24f Add theme from xtermjs.org and make it the demo default 2022-07-23 14:08:15 -07:00
Daniel Imms 91a362be7b Add 3 sample themes to demo 2022-07-23 13:58:31 -07:00
Daniel Imms 19fb806214 Fix tests 2022-07-23 13:44:12 -07:00
Daniel Imms 70b5e0249c Underline demo formatting 2022-07-23 11:22:40 -07:00
Daniel Imms d9703aa7fe Webgl underline color rendering 2022-07-23 11:19:35 -07:00
Daniel Imms 03f15a7fb7 Store extended attributes in single number 2022-07-23 11:12:17 -07:00
Daniel Imms 8cd01dc421 Add underline color test to demo 2022-07-23 10:38:58 -07:00
Daniel Imms 1f89223c2a DOM renderer underline color 2022-07-23 10:26:20 -07:00
Daniel Imms 8287e783a2 Underline color canvas 2022-07-23 10:24:51 -07:00
Daniel Imms 8184e0824b Add tests for dom underline styles 2022-07-23 10:13:26 -07:00
Daniel Imms 72d18f7d5e DOM renderer underline style 2022-07-23 09:20:53 -07:00
Daniel Imms 1d9b4d4778 Canvas renderer underline style 2022-07-23 08:58:10 -07:00
Daniel Imms baa8a1e6ec Stub out canvas renderer underline style 2022-07-23 08:27:15 -07:00
Daniel Imms c116fa645c Render underline style on webgl 2022-07-23 08:23:02 -07:00
Daniel Imms 8b4e8199d7 Get underline style affecting webgl rendering 2022-07-23 07:38:34 -07:00
Daniel Imms 34cd5966b9 Webgl 3 key cache map 2022-07-23 07:31:51 -07:00
Daniel Imms cc8748313e Expose underline attrs as getter/setter
In preparation for storing them in a packed format for glyph caching
2022-07-23 07:13:29 -07:00
Daniel Imms 1207acc495 Add underline test button to demo 2022-07-23 07:05:52 -07:00
Daniel Imms c29cd83213 Allow start task to be restarted without killing watch 2022-07-23 06:39:41 -07:00
Svante Boberg 9df996cb3d Dispose events
https://github.com/microsoft/vscode/issues/155232

The events that is setup for needs to be disposed for js to be able
to garbage collect the terminals.
2022-07-23 13:28:52 +02:00
Daniel Imms d2ac7fc90c Merge pull request #3916 from SvanT/master
Fix memory leak
2022-07-22 05:11:54 -07:00
Svante Boberg 92855ba399 Remove terminal from cache on disposal 2022-07-22 13:03:07 +02:00
Daniel Imms b5b7a5d7d0 Remove note about IE11 which is EOL 2022-07-21 15:24:44 -07:00
Daniel Imms 36830266e2 Merge pull request #3913 from xtermjs/dependabot/npm_and_yarn/terser-5.14.2
Bump terser from 5.7.0 to 5.14.2
2022-07-20 06:10:26 -07:00
dependabot[bot] 3ffbcbf201 Bump terser from 5.7.0 to 5.14.2
Bumps [terser](https://github.com/terser/terser) from 5.7.0 to 5.14.2.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-07-20 01:30:11 +00:00
Daniel Imms 9997f263a6 Merge pull request #3912 from silamon/extendedcolors2
Allow unsetting extending ansi colors
2022-07-19 11:19:57 -07:00
Simon Lamon 1fdbd504c7 Add another unit test to test partial unset 2022-07-19 17:37:02 +00:00
Simon Lamon 06627cb0bb Allow unsetting extending ansicolors 2022-07-19 17:22:54 +00:00
Daniel Imms 2a0681f0c0 Merge pull request #3909 from Tyriar/3905_followup
Be more defensive when setting extended ansi colors
2022-07-19 06:08:50 -07:00
Daniel Imms 67f83d51c9 Fix min color count 2022-07-19 05:58:20 -07:00
Daniel Imms abe7dddfbc Merge pull request #3908 from uellenberg/patch-1
Add Cratecode to Real-world uses
2022-07-19 05:18:45 -07:00
Daniel Imms 8b3f9ad8e1 Be more defensive when setting extended ansi colors
Part of #3601
2022-07-19 05:10:31 -07:00
uellenberg f26264ac23 Add Cratecode to Real-world uses 2022-07-18 15:45:53 -07:00
Daniel Imms cbb13aa4d4 Merge pull request #3905 from silamon/extendedcolors
Support setting extended ansi colors (16-255)
2022-07-18 10:46:37 -07:00
Daniel Imms 86cfad545a Merge pull request #3907 from silamon/devcontainerdeprecation
Fix devcontainer docker build
2022-07-18 10:42:19 -07:00
Simon Lamon c786c6a072 Apply review comments 2022-07-18 17:35:13 +00:00
Simon Lamon 8153aea64c Fix devcontainer docker build 2022-07-18 17:05:34 +00:00
Daniel Imms ebc1c73585 Merge pull request #3906 from silamon/macosdeprecation
Update MacOS base image
2022-07-18 09:32:59 -07:00
Simon Lamon 306efa4d82 Update macos base image 2022-07-18 18:22:33 +02:00
Daniel Imms 0fbd980d38 Whitespace to trigger webgl release 2022-07-13 11:44:08 -07:00
Daniel Imms 0a50c0d27e Merge pull request #3903 from Tyriar/3902
Fix \u259B rendering
2022-07-13 10:13:56 -07:00
Daniel Imms a8405b86de Fix \u259B rendering
Fixes #3902
2022-07-13 02:59:56 -07:00
Daniel Imms 6b447951f1 Merge pull request #3897 from Tyriar/3892
Clear glyph pixels when all channels differ below a threshold
2022-07-11 12:06:33 -07:00
Daniel Imms 788f248de4 Merge pull request #3896 from Tyriar/3877
Clip pixels to left of cell when background color changes
2022-07-11 11:43:29 -07:00
Daniel Imms 9336d7f776 Merge pull request #3900 from Tyriar/3477
Throw when setting an invalid cursorStyle
2022-07-11 11:42:59 -07:00
Daniel Imms 685822df0d Merge pull request #3899 from Tyriar/3898
Fix allowTransparency not working when set after ctor
2022-07-11 11:42:51 -07:00
Daniel Imms c26f605d4c Merge pull request #3895 from Tyriar/3894
Make corner box chars more round
2022-07-11 11:42:33 -07:00
Daniel Imms 0525f43b34 Merge pull request #3901 from Tyriar/3885_2
Be more defensive around SortedList key
2022-07-11 11:35:18 -07:00
Daniel Imms f61ff9fd46 Fix lint 2022-07-11 05:56:42 -07:00
Daniel Imms adcf841a39 Be more defensive around SortedList key
Fixes #3885
Part of microsoft/vscode#151225
2022-07-11 05:50:20 -07:00
Daniel Imms 71195b5370 Throw when setting an invalid cursorStyle
Fixes #3477
2022-07-10 11:43:45 -07:00
Daniel Imms e2b8002d4a Fix allowTransparency not working when set after ctor
Fixes #3898
2022-07-10 08:54:56 -07:00
Daniel Imms ea79194925 Make the clear threshold relative to fg/bg difference 2022-07-10 08:49:09 -07:00
Daniel Imms 89b320a5e2 Clear glyph pixels when all channels differ < 35
Fixes #3892
Also helps with #3877
2022-07-10 07:22:33 -07:00
Daniel Imms 5e8133b4be Clip pixels to left of cell when background color changes
Fixes #3877
2022-07-10 06:54:39 -07:00
Daniel Imms 4cbfd86db0 Make corder box chars more round
Fixes #3894
2022-07-10 05:44:44 -07:00
Daniel Imms b6885a67e7 Merge pull request #3893 from xtermjs/Tyriar-patch-1
Whitespace change to trigger webgl release
2022-07-08 12:53:47 -07:00
Daniel Imms 9f3e3f1700 Comment change to trigger webgl release 2022-07-08 12:53:25 -07:00
Daniel Imms 8f6e473aa7 Whitespace change to trigger webgl release 2022-07-08 12:51:37 -07:00
Daniel Imms 683f85f93d Merge pull request #3891 from sbatten/sbatten/powerline-circles
add powerline semi circle glyphs
2022-07-06 19:01:55 -07:00
Daniel Imms 8959d6ae1d Fix indentation 2022-07-06 18:52:24 -07:00
Megan Rogge 17b1eed0d2 Update src/browser/renderer/CustomGlyphs.ts 2022-07-06 21:50:09 -04:00
Megan Rogge 72eb7e245d Update src/browser/renderer/CustomGlyphs.ts 2022-07-06 21:49:38 -04:00
Daniel Imms 28108fbe2a Add semi-circles to demo 2022-07-06 18:41:47 -07:00
Daniel Imms 83551eaf60 Remove horizontal padding for semi-circles 2022-07-06 18:39:31 -07:00
SteVen Batten 56a1a7613c add powerline semi circle glyphs 2022-07-06 17:13:47 -07:00
Daniel Imms c580b6bf4c Merge pull request #3890 from Tyriar/dispose_webgl
Dispose of webgl resources properly
2022-07-06 07:51:49 -07:00
Daniel Imms 54cbbf600d Dispose of webgl resources properly
Fixes #3889
2022-07-06 07:32:42 -07:00
Daniel Imms d441bd3e53 Merge pull request #3888 from Tyriar/3885
Prevent exception when deleting non-existent element
2022-07-06 07:23:38 -07:00
Daniel Imms 098b2252d0 Prevent exception when deleting non-existent element
Fixes #3885
2022-07-06 04:39:12 -07:00
Daniel Imms 915fa49ce3 Merge pull request #3883 from wuchihsu/add-go-ssh-web-client-to-list-of-xterm-users
Add Go SSH Web Client to list of xterm users
2022-07-05 06:03:17 -07:00
Daniel Imms aa7ff23929 Merge branch 'master' into add-go-ssh-web-client-to-list-of-xterm-users 2022-07-05 05:53:49 -07:00
Daniel Imms 1102f5a4d0 Merge pull request #3884 from web3os-org/master
Update README.md to add web3os to Real-world Uses
2022-07-05 05:53:06 -07:00
Jay Mathis 9aa5a2452a Update README.md
Added link to web3os under Real-world uses
2022-07-05 02:41:03 -05:00
wuchihsu edea440cd8 Add Go SSH Web Client to list of xterm users 2022-07-04 11:26:40 +08:00
Daniel Imms 28ef0c85be Merge pull request #3882 from jerch/fix_3881
fix return type declaration of proposeDimensions
2022-07-01 03:35:07 -07:00
Jörg Breitbart d05cfcd6a7 fix return type declaration of proposeDimensions 2022-07-01 00:22:45 +02:00
Simon Lamon a54426ccb8 extended colors 2022-06-04 12:39:43 +00:00
228 changed files with 10481 additions and 9635 deletions
-29
View File
@@ -1,29 +0,0 @@
FROM node:14
# Configure apt
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get -y install --no-install-recommends apt-utils 2>&1
# Verify git and process tools are installed
RUN apt-get install -y git procps
# Install yarn, puppeteer deps
RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \
&& apt-get -y install --no-install-recommends \
yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
# https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352
gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
# Clean up
RUN apt-get autoremove -y \
&& apt-get clean -y \
&& rm -rf /var/lib/apt/lists/*
ENV DEBIAN_FRONTEND=dialog
+17 -7
View File
@@ -1,10 +1,20 @@
{
"name": "xterm.js",
"dockerFile": "Dockerfile",
"appPort": 3000,
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
"features": {
"ghcr.io/devcontainers/features/node:1": {} // yarn
},
"forwardPorts": [
3000
],
"postCreateCommand": "yarn install",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
}
}
}
+43 -2
View File
@@ -7,6 +7,7 @@
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": [
"demo/tsconfig.json",
"src/browser/tsconfig.json",
"src/common/tsconfig.json",
"src/headless/tsconfig.json",
@@ -14,6 +15,7 @@
"test/benchmark/tsconfig.json",
"addons/xterm-addon-attach/src/tsconfig.json",
"addons/xterm-addon-attach/test/tsconfig.json",
"addons/xterm-addon-canvas/src/tsconfig.json",
"addons/xterm-addon-fit/src/tsconfig.json",
"addons/xterm-addon-fit/test/tsconfig.json",
"addons/xterm-addon-ligatures/src/tsconfig.json",
@@ -37,7 +39,8 @@
"**/*.js"
],
"plugins": [
"@typescript-eslint"
"@typescript-eslint",
"jsdoc"
],
"rules": {
"no-extra-semi": "error",
@@ -94,7 +97,19 @@
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
// memberLike - Allow enum-like objects to use UPPER_CASE
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
// 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"] }
@@ -127,6 +142,9 @@
"warn",
"always"
],
"jsdoc/check-alignment": 1,
"jsdoc/check-param-names": 1,
"jsdoc/no-multi-asterisks": 1,
"keyword-spacing": "warn",
"new-parens": "warn",
"no-duplicate-imports": "warn",
@@ -146,6 +164,29 @@
]
}
],
"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-var": "warn",
+6 -3
View File
@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://stackoverflow.com/questions/tagged/xtermjs
about: Please ask and answer questions here.
- name: FAQ
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
about: See our frequently asked questions before filing a bug report
- name: Support / Q&A
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
about: Use GitHub Discussions for community support and general Q&A
+72
View File
@@ -0,0 +1,72 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '41 17 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
-6
View File
@@ -1,11 +1,5 @@
require:
- source-map-support/register
spec:
- out/**/*.test.js
- addons/**/out/*.test.js
watch-files:
- out/**/*.js
- addons/**/out/*.js
reporter: spec
color: true
check-leaks: true
+11 -3
View File
@@ -1,5 +1,13 @@
{
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
"mochaExplorer.envPath": ".mocha.env"
"typescript.preferences.importModuleSpecifier": "non-relative",
"typescript.preferences.quoteStyle": "single",
"mochaExplorer.envPath": ".mocha.env",
"mochaExplorer.files": [
"out/**/*.test.js",
"addons/**/out/*.test.js"
],
"mochaExplorer.watch": [
"out/**/*.js",
"addons/**/out/*.js"
]
}
+1 -2
View File
@@ -32,7 +32,6 @@
"label": "start",
"type": "npm",
"script": "start",
"dependsOn": "watch",
"group": "build",
"isBackground": true,
"problemMatcher": [],
@@ -42,7 +41,7 @@
},
{
"label": "Start demo",
"dependsOn": "start",
"dependsOn": ["start", "watch"],
"group": {
"kind": "build",
"isDefault": true
+29 -25
View File
@@ -87,8 +87,6 @@ The xterm.js team maintains the following addons, but anyone can build them:
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*.
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
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.
### Node.js Support
@@ -101,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file](
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.
## 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).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash
npm install -S 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.
## 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.
## Real-world uses
Xterm.js is used in several world-class applications to provide great terminal experiences.
@@ -142,7 +160,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**GoTTY**](https://github.com/sorenisanerd/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
@@ -168,7 +186,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
- [**Replit**](https://replit.com): Collaborative browser based IDE with support for 50+ different languages.
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
@@ -192,34 +210,20 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
- [**CloudTTY**](https://github.com/cloudtty/cloudtty): A Friendly Kubernetes CloudShell (Web Terminal).
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
- [**graSSHopper**](https://grasshopper.coding.kiwi): A simple SSH client with file explorer, history and many more features.
- [**DomTerm**](https://domterm.org/xtermjs.html): Tiles and tabs. Detachable sessions (like tmux). [Remote connections](https://domterm.org/Remoting-over-ssh.html) using a nice ssh wrapper with predictive echo. Qt, Electron, Tauri/Wry, or desktop browser front-ends. Choose between xterm.js engine (faster) or native DomTerm (more functionality and graphics) - or both.
- [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.
## 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).
### Beta builds
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash
npm install -S 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.
## 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.
## 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-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2017-2022, [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 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-attach",
"version": "0.6.0",
"version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,6 +21,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
+18 -4
View File
@@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon {
}
private _sendData(data: string): void {
// TODO: do something better than just swallowing
// the data if the socket is not in a working condition
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
this._socket.send(data);
}
private _sendBinary(data: string): void {
if (this._socket.readyState !== 1) {
if (!this._checkOpenSocket()) {
return;
}
const buffer = new Uint8Array(data.length);
@@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon {
}
this._socket.send(buffer);
}
private _checkOpenSocket(): boolean {
switch (this._socket.readyState) {
case WebSocket.OPEN:
return true;
case WebSocket.CONNECTING:
throw new Error('Attach addon was loaded before socket was open');
case WebSocket.CLOSING:
console.warn('Attach addon socket is closing');
return false;
case WebSocket.CLOSED:
throw new Error('Attach addon socket is closed');
default:
throw new Error('Unexpected socket state');
}
}
}
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -28,7 +28,7 @@ describe('AttachAddon', () => {
beforeEach(async () => await page.goto(APP));
it('string', async function(): Promise<any> {
await openTerminal(page, { rendererType: 'dom' });
await openTerminal(page);
const port = 8080;
const server = new WebSocket.Server({ port });
server.on('connection', socket => socket.send('foo'));
@@ -38,7 +38,7 @@ describe('AttachAddon', () => {
});
it('utf8', async function(): Promise<any> {
await openTerminal(page, { rendererType: 'dom' });
await openTerminal(page);
const port = 8080;
const server = new WebSocket.Server({ port });
const data = new Uint8Array([102, 111, 111]);
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-attach' {
export interface IAttachOptions {
+2
View File
@@ -0,0 +1,2 @@
lib
node_modules
+29
View File
@@ -0,0 +1,29 @@
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
!src/**/*.d.ts
!src/**/*.js
!src/**/*.js.map
!src/**/*.css
# Blacklist - src/ test files
src/**/*.test.ts
src/**/*.test.d.ts
src/**/*.test.js
src/**/*.test.js.map
# Whitelist - typings/
!typings/*.d.ts
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2017, 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.
+28
View File
@@ -0,0 +1,28 @@
## xterm-addon-canvas
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a canvas-based renderer using a 2d context to draw. This addon requires xterm.js v5+.
The purpose of this addon is to be used as a fallback for the [webgl addon](https://www.npmjs.com/package/xterm-addon-webgl) when better performance is desired over the default DOM renderer, but WebGL2 isn't supported or performant for some reason.
### Install
```bash
npm install --save xterm-addon-canvas
```
### Usage
```ts
import { Terminal } from 'xterm';
import { CanvasAddon } from 'xterm-addon-canvas';
const terminal = new Terminal();
terminal.open(element);
terminal.loadAddon(new CanvasAddon());
```
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts) for more advanced usage.
### See also
- [xterm-addon-webgl](https://www.npmjs.com/package/xterm-addon-webgl) A renderer for xterm.js that uses WebGL
+27
View File
@@ -0,0 +1,27 @@
{
"name": "xterm-addon-canvas",
"version": "0.4.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-canvas.js",
"types": "typings/xterm-addon-canvas.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"canvas",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^5.0.0"
}
}
@@ -0,0 +1,496 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ReadonlyColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICellData, IDisposable } from 'common/Types';
import { Terminal } from 'xterm';
import { IRenderLayer } from './Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { isSafari } from 'common/Platform';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _deviceCharWidth: number = 0;
private _deviceCharHeight: number = 0;
private _deviceCellWidth: number = 0;
private _deviceCellHeight: number = 0;
private _deviceCharLeft: number = 0;
private _deviceCharTop: number = 0;
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
private _cellColorResolver: CellColorResolver;
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
protected _charAtlas!: ITextureAtlas;
private _charAtlasDisposable?: IDisposable;
public get canvas(): HTMLCanvasElement { return this._canvas; }
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
constructor(
private readonly _terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected readonly _themeService: IThemeService,
protected readonly _bufferService: IBufferService,
protected readonly _optionsService: IOptionsService,
protected readonly _decorationService: IDecorationService,
protected readonly _coreBrowserService: ICoreBrowserService
) {
super();
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
this._refreshCharAtlas(this._themeService.colors);
this.register(this._themeService.onChangeColors(e => {
this._refreshCharAtlas(e);
this.reset();
// Trigger selection changed as it's handled separately to regular rendering
this.handleSelectionChanged(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}));
this.register(toDisposable(() => {
this._canvas.remove();
this._charAtlas?.dispose();
}));
}
private _initCanvas(): void {
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
}
public handleBlur(): void {}
public handleFocus(): void {}
public handleCursorMove(): void {}
public handleGridChanged(startRow: number, endRow: number): void {}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._selectionModel.update(this._terminal, start, end, columnSelectMode);
}
protected _setTransparency(alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(this._themeService.colors);
this.handleGridChanged(0, this._bufferService.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlasDisposable?.dispose();
this._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
this._charAtlas.warmUp();
for (let i = 0; i < this._charAtlas.pages.length; i++) {
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
}
}
public resize(dim: IRenderDimensions): void {
this._deviceCellWidth = dim.device.cell.width;
this._deviceCellHeight = dim.device.cell.height;
this._deviceCharWidth = dim.device.char.width;
this._deviceCharHeight = dim.device.char.height;
this._deviceCharLeft = dim.device.char.left;
this._deviceCharTop = dim.device.char.top;
this._canvas.width = dim.device.canvas.width;
this._canvas.height = dim.device.canvas.height;
this._canvas.style.width = `${dim.css.canvas.width}px`;
this._canvas.style.height = `${dim.css.canvas.height}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
this._refreshCharAtlas(this._themeService.colors);
}
public abstract reset(): void;
public clearTextureAtlas(): void {
this._charAtlas?.clearTexture();
}
/**
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
* @param x The column to start at.
* @param y The row to start at
* @param width The number of columns to fill.
* @param height The number of rows to fill.
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
/**
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
const cellOffset = Math.ceil(this._deviceCellHeight * 0.5);
this._ctx.fillRect(
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
/**
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void {
this._ctx.save();
this._ctx.beginPath();
this._ctx.strokeStyle = this._ctx.fillStyle;
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
for (let xOffset = 0; xOffset < width; xOffset++) {
const xLeft = (x + xOffset) * this._deviceCellWidth;
const xMid = (x + xOffset + 0.5) * this._deviceCellWidth;
const xRight = (x + xOffset + 1) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
const yMidBot = yMid - lineWidth;
const yMidTop = yMid + lineWidth;
this._ctx.moveTo(xLeft, yMid);
this._ctx.bezierCurveTo(
xLeft, yMidBot,
xMid, yMidBot,
xMid, yMid
);
this._ctx.bezierCurveTo(
xMid, yMidTop,
xRight, yMidTop,
xRight, yMid
);
}
this._ctx.stroke();
this._ctx.restore();
}
protected _dottedUnderlineAtCell(x: number, y: number, width: number = 1): void {
this._ctx.save();
this._ctx.beginPath();
this._ctx.strokeStyle = this._ctx.fillStyle;
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
const xLeft = x * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
for (let xOffset = 0; xOffset < width; xOffset++) {
// const xLeft = x * this._deviceCellWidth;
const xRight = (x + width + xOffset) * this._deviceCellWidth;
this._ctx.lineTo(xRight, yMid);
}
this._ctx.stroke();
this._ctx.closePath();
this._ctx.restore();
}
protected _dashedUnderlineAtCell(x: number, y: number, width: number = 1): void {
this._ctx.save();
this._ctx.beginPath();
this._ctx.strokeStyle = this._ctx.fillStyle;
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
const xLeft = x * this._deviceCellWidth;
const xRight = (x + width) * this._deviceCellWidth;
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
this._ctx.moveTo(xLeft, yMid);
this._ctx.lineTo(xRight, yMid);
this._ctx.stroke();
this._ctx.closePath();
this._ctx.restore();
}
/**
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
* existing fillStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
this._coreBrowserService.dpr * width,
this._deviceCellHeight);
}
/**
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
* strokeStyle on the context.
* @param x The column to fill.
* @param y The row to fill.
*/
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
const lineWidth = this._coreBrowserService.dpr;
this._ctx.lineWidth = lineWidth;
this._ctx.strokeRect(
x * this._deviceCellWidth + lineWidth / 2,
y * this._deviceCellHeight + (lineWidth / 2),
width * this._deviceCellWidth - lineWidth,
(height * this._deviceCellHeight) - lineWidth);
}
/**
* Clears the entire canvas.
*/
protected _clearAll(): void {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
/**
* Clears 1+ cells completely.
* @param x The column to start at.
* @param y The row to start at.
* @param width The number of columns to clear.
* @param height The number of rows to clear.
*/
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
} else {
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
}
/**
* Draws a truecolor character at the cell. The character will be clipped to
* ensure that it fits with the cell, including the cell to the right if it's
* a wide character. This uses the existing fillStyle on the context.
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = TEXT_BASELINE;
this._clipRow(y);
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
}
// Draw the character
if (!drawSuccess) {
this._ctx.fillText(
cell.getChars(),
x * this._deviceCellWidth + this._deviceCharLeft,
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
}
}
/**
* Draws one or more characters at a cell. If possible this will draw using
* the character atlas to reduce draw time.
*/
protected _drawChars(cell: ICellData, x: number, y: number): void {
const chars = cell.getChars();
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
let glyph: IRasterizedGlyph;
if (chars && chars.length > 1) {
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
} else {
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
}
if (!glyph.size.x || !glyph.size.y) {
return;
}
this._ctx.save();
this._clipRow(y);
// Draw the image, use the bitmap if it's available
// HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but
// something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are
// swapped out unexpectedly
if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) {
this._bitmapGenerator[glyph.texturePage]?.bitmap?.close();
delete this._bitmapGenerator[glyph.texturePage];
}
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
if (!this._bitmapGenerator[glyph.texturePage]) {
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
}
this._bitmapGenerator[glyph.texturePage]!.refresh();
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
}
this._ctx.drawImage(
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
glyph.texturePosition.x,
glyph.texturePosition.y,
glyph.size.x,
glyph.size.y,
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
glyph.size.x,
glyph.size.y
);
this._ctx.restore();
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param y The row to clip.
*/
private _clipRow(y: number): void {
this._ctx.beginPath();
this._ctx.rect(
0,
y * this._deviceCellHeight,
this._bufferService.cols * this._deviceCellWidth,
this._deviceCellHeight);
this._ctx.clip();
}
/**
* Gets the current font.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
}
}
/**
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
* the operation as window.createImageBitmap is asynchronous.
*/
const GLYPH_BITMAP_COMMIT_DELAY = 100;
const enum BitmapGeneratorState {
IDLE = 0,
GENERATING = 1,
GENERATING_INVALID = 2
}
class BitmapGenerator {
private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
private _commitTimeout: number | undefined = undefined;
private _bitmap: ImageBitmap | undefined = undefined;
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
public version: number = -1;
constructor(public readonly canvas: HTMLCanvasElement) {
}
public refresh(): void {
// Clear the bitmap immediately as it's stale
this._bitmap?.close();
this._bitmap = undefined;
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
if (isSafari) {
return;
}
if (this._commitTimeout === undefined) {
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
}
if (this._state === BitmapGeneratorState.GENERATING) {
this._state = BitmapGeneratorState.GENERATING_INVALID;
}
}
private _generate(): void {
if (this._state === BitmapGeneratorState.IDLE) {
this._bitmap?.close();
this._bitmap = undefined;
this._state = BitmapGeneratorState.GENERATING;
window.createImageBitmap(this.canvas).then(bitmap => {
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
this.refresh();
} else {
this._bitmap = bitmap;
}
this._state = BitmapGeneratorState.IDLE;
});
if (this._commitTimeout) {
this._commitTimeout = undefined;
}
}
}
}
@@ -0,0 +1,62 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { CanvasRenderer } from './CanvasRenderer';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
export class CanvasAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: CanvasRenderer;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
}
public activate(terminal: Terminal): void {
const core = (terminal as any)._core as ITerminal;
if (!terminal.element) {
this.register(core.onWillOpen(() => this.activate(terminal)));
return;
}
this._terminal = terminal;
const coreService = core.coreService;
const optionsService = core.optionsService;
const screenElement = core.screenElement!;
const linkifier = core.linkifier2;
const unsafeCore = core as any;
const bufferService: IBufferService = unsafeCore._bufferService;
const renderService: IRenderService = unsafeCore._renderService;
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
const decorationService: IDecorationService = unsafeCore._decorationService;
const themeService: IThemeService = unsafeCore._themeService;
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
renderService.setRenderer(this._renderer);
renderService.handleResize(bufferService.cols, bufferService.rows);
this.register(toDisposable(() => {
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.handleResize(terminal.cols, terminal.rows);
this._renderer?.dispose();
this._renderer = undefined;
}));
}
}
@@ -0,0 +1,186 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, ISelectionService, IThemeService } from 'browser/services/Services';
import { ILinkifier2 } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { CursorRenderLayer } from './CursorRenderLayer';
import { LinkRenderLayer } from './LinkRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { TextRenderLayer } from './TextRenderLayer';
import { IRenderLayer } from './Types';
export class CanvasRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
public dimensions: IRenderDimensions;
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
public readonly onRequestRedraw = this._onRequestRedraw.event;
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
constructor(
private readonly _terminal: Terminal,
private readonly _screenElement: HTMLElement,
linkifier2: ILinkifier2,
private readonly _bufferService: IBufferService,
private readonly _charSizeService: ICharSizeService,
private readonly _optionsService: IOptionsService,
characterJoinerService: ICharacterJoinerService,
coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
private readonly _themeService: IThemeService
) {
super();
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService),
new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService),
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
];
for (const layer of this._renderLayers) {
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
}
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this.register(toDisposable(() => {
for (const l of this._renderLayers) {
l.dispose();
}
removeTerminalFromCache(this._terminal);
}));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderLayers[0].cacheCanvas;
}
public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
}
public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
// Resize the screen
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
}
public handleCharSizeChanged(): void {
this.handleResize(this._bufferService.cols, this._bufferService.rows);
}
public handleBlur(): void {
this._runOperation(l => l.handleBlur());
}
public handleFocus(): void {
this._runOperation(l => l.handleFocus());
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode));
// Selection foreground requires a full re-render
if (this._themeService.colors.selectionForeground) {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
public handleCursorMove(): void {
this._runOperation(l => l.handleCursorMove());
}
public clear(): void {
this._runOperation(l => l.reset());
}
private _runOperation(operation: (layer: IRenderLayer) => void): void {
for (const l of this._renderLayers) {
operation(l);
}
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
public renderRows(start: number, end: number): void {
for (const l of this._renderLayers) {
l.handleGridChanged(start, end);
}
}
public clearTextureAtlas(): void {
for (const layer of this._renderLayers) {
layer.clearTextureAtlas();
}
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
if (!this._charSizeService.hasValidSize) {
return;
}
// See the WebGL renderer for an explanation of this section.
const dpr = this._coreBrowserService.dpr;
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr);
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width;
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
}
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
this.dimensions.device.canvas.height = height;
this.dimensions.device.canvas.width = width;
// Resize all render layers
for (const l of this._renderLayers) {
l.resize(this.dimensions);
}
this._requestRedrawViewport();
}
private _requestRedrawViewport(): void {
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
}
}
@@ -3,14 +3,17 @@
* @license MIT
*/
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Terminal } from 'xterm';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
interface ICursorState {
x: number;
@@ -32,18 +35,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cell: ICellData = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService,
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
@IDecorationService decorationService: IDecorationService
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _coreService: ICoreService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
themeService: IThemeService
) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = {
x: 0,
y: 0,
@@ -56,14 +59,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
}
public dispose(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
this.register(toDisposable(() => {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}
super.dispose();
}));
}
public resize(dim: IRenderDimensions): void {
@@ -81,25 +81,25 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation();
this.onOptionsChanged();
this._handleOptionsChanged();
}
public onBlur(): void {
public handleBlur(): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
public handleFocus(): void {
this._cursorBlinkStateManager?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onOptionsChanged(): void {
private _handleOptionsChanged(): void {
if (this._optionsService.rawOptions.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
});
}, this._coreBrowserService);
}
} else {
this._cursorBlinkStateManager?.dispose();
@@ -110,11 +110,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onCursorMove(): void {
public handleCursorMove(): void {
this._cursorBlinkStateManager?.restartBlinkAnimation();
}
public onGridChanged(startRow: number, endRow: number): void {
public handleGridChanged(startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(false);
} else {
@@ -148,13 +148,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
}
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
@@ -195,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (window.devicePixelRatio < 1) {
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
// less than 1
if (isFirefox || this._coreBrowserService.dpr < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
@@ -213,30 +210,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._colors.cursorAccent.css;
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor.css;
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
@@ -258,7 +255,8 @@ class CursorBlinkStateManager {
constructor(
isFocused: boolean,
private _renderCallback: () => void
private _renderCallback: () => void,
private _coreBrowserService: ICoreBrowserService
) {
this.isCursorVisible = true;
if (isFocused) {
@@ -270,15 +268,15 @@ class CursorBlinkStateManager {
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -292,7 +290,7 @@ class CursorBlinkStateManager {
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
@@ -302,7 +300,7 @@ class CursorBlinkStateManager {
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
@@ -310,7 +308,7 @@ class CursorBlinkStateManager {
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = window.setTimeout(() => {
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
@@ -324,13 +322,13 @@ class CursorBlinkStateManager {
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = window.setInterval(() => {
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
@@ -343,7 +341,7 @@ class CursorBlinkStateManager {
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
@@ -354,15 +352,15 @@ class CursorBlinkStateManager {
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { GridCache } from 'browser/renderer/GridCache';
import { GridCache } from './GridCache';
describe('GridCache', () => {
let grid: GridCache<number>;
@@ -3,33 +3,33 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
import { Terminal } from 'xterm';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
linkifier: ILinkifier,
linkifier2: ILinkifier2,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService
bufferService: IBufferService,
optionsService: IOptionsService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService,
themeService: IThemeService
) {
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(dim: IRenderDimensions): void {
@@ -54,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
private _onShowLinkUnderline(e: ILinkifierEvent): void {
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
@@ -78,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
this._state = e;
}
private _onHideLinkUnderline(e: ILinkifierEvent): void {
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
@@ -3,10 +3,11 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { Terminal } from 'xterm';
interface ISelectionState {
start?: [number, number];
@@ -19,15 +20,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
private _state!: ISelectionState;
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
rendererId: number,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@IDecorationService decorationService: IDecorationService
bufferService: IBufferService,
coreBrowserService: ICoreBrowserService,
decorationService: IDecorationService,
optionsService: IOptionsService,
themeService: IThemeService
) {
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._clearState();
}
@@ -42,8 +44,12 @@ export class SelectionRenderLayer extends BaseRenderLayer {
public resize(dim: IRenderDimensions): void {
super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._clearState();
// On resize use the base render layer's cached selection values since resize clears _state
// inside reset.
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
this._clearState();
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
}
public reset(): void {
@@ -53,9 +59,22 @@ export class SelectionRenderLayer extends BaseRenderLayer {
}
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.onSelectionChanged(start, end, columnSelectMode);
public handleBlur(): void {
this.reset();
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public handleFocus(): void {
this.reset();
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
super.handleSelectionChanged(start, end, columnSelectMode);
this._redrawSelection(start, end, columnSelectMode);
}
private _redrawSelection(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
// Selection has not changed
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
return;
@@ -82,7 +101,9 @@ export class SelectionRenderLayer extends BaseRenderLayer {
return;
}
this._ctx.fillStyle = this._colors.selectionTransparent.css;
this._ctx.fillStyle = (this._coreBrowserService.isFocused
? this._themeService.colors.selectionBackgroundTransparent
: this._themeService.colors.selectionInactiveBackgroundTransparent).css;
if (columnSelectMode) {
const startCol = start[0];
@@ -3,17 +3,19 @@
* @license MIT
*/
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { CharData, ICellData } from 'common/Types';
import { GridCache } from 'browser/renderer/GridCache';
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
import { AttributeData } from 'common/buffer/AttributeData';
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
import { IColorSet } from 'browser/Types';
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
import { ICharacterJoinerService } from 'browser/services/Services';
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
import { color, css } from 'common/Color';
import { Terminal } from 'xterm';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -30,18 +32,20 @@ export class TextRenderLayer extends BaseRenderLayer {
private _workCell = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
alpha: boolean,
rendererId: number,
@IBufferService bufferService: IBufferService,
@IOptionsService optionsService: IOptionsService,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IDecorationService decorationService: IDecorationService
bufferService: IBufferService,
optionsService: IOptionsService,
private readonly _characterJoinerService: ICharacterJoinerService,
decorationService: IDecorationService,
coreBrowserService: ICoreBrowserService,
themeService: IThemeService
) {
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService);
super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
this._state = new GridCache<CharData>();
this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
}
public resize(dim: IRenderDimensions): void {
@@ -49,8 +53,8 @@ export class TextRenderLayer extends BaseRenderLayer {
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
this._characterWidth = dim.device.char.width;
this._characterFont = terminalFont;
this._characterOverlapCache = {};
}
@@ -91,6 +95,13 @@ export class TextRenderLayer extends BaseRenderLayer {
continue;
}
// exit early for NULL and SP
// NOTE: commented out due to #4120 (needs a more clever patch to keep things performant)
// const code = cell.getCode();
// if (code === 0 || code === 32) {
// continue;
// }
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
@@ -165,30 +176,30 @@ export class TextRenderLayer extends BaseRenderLayer {
if (cell.isInverse()) {
if (cell.isFgDefault()) {
nextFillStyle = this._colors.foreground.css;
nextFillStyle = this._themeService.colors.foreground.css;
} else if (cell.isFgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
}
} else if (cell.isBgRGB()) {
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else if (cell.isBgPalette()) {
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
}
// Get any decoration foreground/background overrides, this must be fetched before the early
// exist but applied after inverse
let isTop = false;
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
this._decorationService.forEachDecorationAtCell(x, this._bufferService.buffer.ydisp + y, undefined, d => {
if (d.options.layer !== 'top' && isTop) {
continue;
return;
}
if (d.backgroundColorRGB) {
nextFillStyle = d.backgroundColorRGB.css;
}
isTop = d.options.layer === 'top';
}
});
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
@@ -224,52 +235,10 @@ export class TextRenderLayer extends BaseRenderLayer {
}
private _drawForeground(firstRow: number, lastRow: number): void {
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
if (cell.isInvisible()) {
return;
}
this._drawChars(cell, x, y);
if (cell.isUnderline() || cell.isStrikethrough()) {
this._ctx.save();
if (cell.isInverse()) {
if (cell.isBgDefault()) {
this._ctx.fillStyle = this._colors.background.css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
}
} else {
if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
}
}
if (cell.isStrikethrough()) {
this._fillMiddleLineAtCells(x, y, cell.getWidth());
}
if (cell.isUnderline()) {
this._fillBottomLineAtCells(x, y, cell.getWidth());
}
this._ctx.restore();
}
});
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
}
public onGridChanged(firstRow: number, lastRow: number): void {
public handleGridChanged(firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
@@ -284,10 +253,6 @@ export class TextRenderLayer extends BaseRenderLayer {
this._drawForeground(firstRow, lastRow);
}
public onOptionsChanged(): void {
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
@@ -326,19 +291,4 @@ export class TextRenderLayer extends BaseRenderLayer {
this._characterOverlapCache[chars] = overlaps;
return overlaps;
}
/**
* Clear the charcater at the cell specified.
* @param x The column of the char.
* @param y The row of the char.
*/
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
@@ -4,23 +4,8 @@
*/
import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRequestRedrawEvent {
start: number;
@@ -40,57 +25,49 @@ export interface IRenderer extends IDisposable {
*/
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
dispose(): void;
setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
handleDevicePixelRatioChange(): void;
handleResize(cols: number, rows: number): void;
handleCharSizeChanged(): void;
handleBlur(): void;
handleFocus(): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleCursorMove(): void;
handleOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
export interface IRenderLayer extends IDisposable {
readonly canvas: HTMLCanvasElement;
readonly cacheCanvas: HTMLCanvasElement;
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
/**
* Called when the terminal loses focus.
*/
onBlur(): void;
handleBlur(): void;
/**
* * Called when the terminal gets focus.
* Called when the terminal gets focus.
*/
onFocus(): void;
handleFocus(): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(): void;
/**
* Called when options change.
*/
onOptionsChanged(): void;
/**
* Called when the theme changes.
*/
setColors(colorSet: IColorSet): void;
handleCursorMove(): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(startRow: number, endRow: number): void;
handleGridChanged(startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Resize the render layer.
@@ -0,0 +1,40 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"lib": [
"dom",
"es6"
],
"rootDir": ".",
"outDir": "../out",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
"downlevelIteration": true,
"types": [
"../../../node_modules/@types/mocha"
]
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"files": [],
"include": [],
"references": [
{ "path": "./src" }
]
}
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
declare module 'xterm-addon-canvas' {
/**
* An xterm.js addon that provides search functionality.
*/
export class CanvasAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
/**
* An event that is fired when the a new page is added to the texture atlas.
*/
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
constructor();
/**
* Activates the addon.
* @param terminal The terminal the addon is being loaded in.
*/
public activate(terminal: Terminal): void;
/**
* Disposes the addon.
*/
public dispose(): void;
/**
* Clears the terminal's texture atlas and triggers a redraw.
*/
public clearTextureAtlas(): void;
}
}
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const path = require('path');
const addonName = 'CanvasAddon';
const mainFile = 'xterm-addon-canvas.js';
module.exports = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
exclude: /node_modules/
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common'),
browser: path.resolve('../../out/browser')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd'
},
mode: 'production'
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-fit",
"version": "0.5.0",
"version": "0.7.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,6 +21,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
+5 -3
View File
@@ -4,6 +4,7 @@
*/
import { Terminal, ITerminalAddon } from 'xterm';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
interface ITerminalDimensions {
/**
@@ -58,8 +59,9 @@ export class FitAddon implements ITerminalAddon {
// TODO: Remove reliance on private API
const core = (this._terminal as any)._core;
const dims: IRenderDimensions = core._renderService.dimensions;
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
return undefined;
}
@@ -81,8 +83,8 @@ export class FitAddon implements ITerminalAddon {
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
const geometry = {
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
};
return geometry;
}
+12 -2
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"browser/*": [
"../../../src/browser/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/browser"
}
]
}
+8 -4
View File
@@ -48,7 +48,8 @@ describe('FitAddon', () => {
it('default', async function(): Promise<any> {
await loadFit();
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
assert.equal(dimensions.cols, 87);
assert.isAbove(dimensions.cols, 85);
assert.isBelow(dimensions.cols, 88);
assert.isAbove(dimensions.rows, 24);
assert.isBelow(dimensions.rows, 29);
});
@@ -56,7 +57,8 @@ describe('FitAddon', () => {
it('width', async function(): Promise<any> {
await loadFit(1008);
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
assert.equal(dimensions.cols, 110);
assert.isAbove(dimensions.cols, 108);
assert.isBelow(dimensions.cols, 111);
assert.isAbove(dimensions.rows, 24);
assert.isBelow(dimensions.rows, 29);
});
@@ -89,7 +91,8 @@ describe('FitAddon', () => {
await page.evaluate(`window.fit.fit()`);
const cols: number = await page.evaluate(`window.term.cols`);
const rows: number = await page.evaluate(`window.term.rows`);
assert.equal(cols, 87);
assert.isAbove(cols, 85);
assert.isBelow(cols, 88);
assert.isAbove(rows, 24);
assert.isBelow(rows, 29);
});
@@ -99,7 +102,8 @@ describe('FitAddon', () => {
await page.evaluate(`window.fit.fit()`);
const cols: number = await page.evaluate(`window.term.cols`);
const rows: number = await page.evaluate(`window.term.rows`);
assert.equal(cols, 110);
assert.isAbove(cols, 108);
assert.isBelow(cols, 111);
assert.isAbove(rows, 24);
assert.isBelow(rows, 29);
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
+1 -1
View File
@@ -35,7 +35,7 @@ declare module 'xterm-addon-fit' {
/**
* Gets the proposed dimensions that will be used for a fit.
*/
public proposeDimensions(): ITerminalDimensions;
public proposeDimensions(): ITerminalDimensions | undefined;
}
/**
+6 -5
View File
@@ -33,15 +33,16 @@ This package locates the font file on disk for the font currently in use by the
Since this package depends on being able to find and resolve a system font from disk, it has to have system access that isn't available in the web browser. As a result, this package is mainly useful in environments that combine browser and Node.js runtimes (such as [Electron]).
### Fallback Ligatures
When ligatures cannot be fetched from the environment, a set of "fallback" ligatures is used to get the most common ligatures working. These fallback ligatures can be customized with options passed to `LigatureAddon.constructor`.
### Fonts
This package makes use of the following fonts for testing:
* [Fira Code][Fira Code] - [Licensed under the OFL][Fira Code License] by Nikita
Prokopov, Mozilla Foundation with reserved names Fira Code, Fira Mono, and
Fira Sans
* [Iosevka] - [Licensed under the OFL][Iosevka License] by Belleve Invis with
reserved name Iosevka
* [Fira Code][Fira Code] - [Licensed under the OFL][Fira Code License] by Nikita Prokopov, Mozilla Foundation with reserved names Fira Code, Fira Mono, and Fira Sans
* [Iosevka] - [Licensed under the OFL][Iosevka License] by Belleve Invis with reserved name Iosevka
[xterm.js]: https://github.com/xtermjs/xterm.js
[Electron]: https://electronjs.org/
@@ -22,10 +22,13 @@ const fontsFolder = path.join(__dirname, '../fonts');
async function download() {
await mkdirp(fontsFolder);
await downloadFiraCode();
await downloadIosevka();
console.log('Loaded all fonts for testing')
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() {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.5.3",
"version": "0.6.0",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -42,6 +42,6 @@
"yauzl": "^2.10.0"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
@@ -5,6 +5,7 @@
import { Terminal } from 'xterm';
import { enableLigatures } from '.';
import { ILigatureOptions } from './Types';
export interface ITerminalAddon {
activate(terminal: Terminal): void;
@@ -12,12 +13,31 @@ export interface ITerminalAddon {
}
export class LigaturesAddon implements ITerminalAddon {
constructor() {}
private readonly _fallbackLigatures: string[];
public activate(terminal: Terminal): void {
enableLigatures(terminal);
private _terminal: Terminal | undefined;
private _characterJoinerId: number | undefined;
constructor(options?: Partial<ILigatureOptions>) {
this._fallbackLigatures = (options?.fallbackLigatures || [
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
'++', '+++', '<!--', '<!---', '<***>'
]).sort((a, b) => b.length - a.length);
}
public dispose(): void {}
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
}
public dispose(): void {
if (this._characterJoinerId !== undefined) {
this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
this._characterJoinerId = undefined;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
export interface ILigatureOptions {
fallbackLigatures: string[];
}
+19 -10
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
import { FontList } from 'font-finder';
import { Font, loadBuffer, loadFile } from 'font-ligatures';
import { Font, loadBuffer } from 'font-ligatures';
import parse from './parse';
@@ -24,7 +23,7 @@ interface IFontAccessNavigator {
};
}
let fontsPromise: Promise<FontList | Record<string, IFontMetadata[]>> | undefined = undefined;
let fontsPromise: Promise<Record<string, IFontMetadata[]>> | undefined = undefined;
/**
* Loads the font ligature wrapper for the specified font family if it could be
@@ -65,12 +64,20 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
console.error(err.name, err.message);
}
}
// Node environment or no font access API
else {
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
else if (typeof process !== 'object' && 'queryLocalFonts' in window) {
const fonts: Record<string, IFontMetadata[]> = {};
try {
fontsPromise = (await import('font-finder')).list();
} catch (err) {
// No-op
const fontsIterator = await (window as any).queryLocalFonts();
for (const metadata of fontsIterator) {
if (!fonts.hasOwnProperty(metadata.family)) {
fonts[metadata.family] = [];
}
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err: any) {
console.error(err.name, err.message);
}
}
if (!fontsPromise) {
@@ -90,9 +97,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
const font = fonts[family][0];
if ('blob' in font) {
return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize });
const bytes = await font.blob();
const buffer = await bytes.arrayBuffer();
return loadBuffer(buffer, { cacheSize });
}
return await loadFile(font.path, { cacheSize });
return undefined;
}
}
@@ -69,82 +69,11 @@ describe('xterm-addon-ligatures', () => {
assert.deepEqual(term.joiner!(input), []);
});
it('returns the correct set of ranges once the font has loaded', done => {
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
done();
});
});
it('handles quoted font names', done => {
term.options.fontFamily = '"Fira Code", monospace';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
done();
});
});
it('falls back to later fonts if earlier ones are not present', done => {
term.options.fontFamily = 'notinstalled, Fira Code, monospace';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
done();
});
});
it('uses the current font value', done => {
// The first three calls are all synchronous so that we don't allow time for
// any fonts to load while we're switching things around
term.options.fontFamily = 'Fira Code';
assert.deepEqual(term.joiner!(input), []);
term.options.fontFamily = 'notinstalled';
assert.deepEqual(term.joiner!(input), []);
term.options.fontFamily = 'Iosevka';
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4]]);
// And switch it back to Fira Code for good measure
term.options.fontFamily = 'Fira Code';
// At this point, we haven't loaded the new font, so the result reverts
// back to empty until that happens
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
done();
});
});
});
it('allows multiple terminal instances that use different fonts', done => {
const onRefresh2 = sinon.stub();
const term2 = new MockTerminal(onRefresh2);
term2.options.fontFamily = 'Iosevka';
ligatureSupport.enableLigatures(term2 as any);
assert.deepEqual(term.joiner!(input), []);
onRefresh.callsFake(() => {
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
assert.deepEqual(term2.joiner!(input), []);
onRefresh2.callsFake(() => {
assert.deepEqual(term2.joiner!(input), [[2, 4]]);
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
done();
});
});
});
it('fails if it finds but cannot load the font', async () => {
term.options.fontFamily = 'Nonexistant Font, monospace';
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.throws(() => term.joiner!(input));
});
it('returns nothing if the font is not present on the system', async () => {
@@ -176,7 +105,6 @@ describe('xterm-addon-ligatures', () => {
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.throws(() => term.joiner!(input));
});
it('ensures no empty errors are thrown', async () => {
@@ -185,7 +113,6 @@ describe('xterm-addon-ligatures', () => {
assert.deepEqual(term.joiner!(input), []);
await delay(500);
assert.isTrue(onRefresh.notCalled);
assert.throws(() => term.joiner!(input), 'Failure while loading font');
(fontLigatures.loadFile as sinon.SinonStub).restore();
});
});
+22 -8
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal } from 'xterm';
import type { Terminal } from 'xterm';
import { Font } from 'font-ligatures';
import load from './font';
@@ -26,13 +26,13 @@ const CACHE_SIZE = 100000;
* start to render them.
* @param term Terminal instance from xterm.js
*/
export function enableLigatures(term: Terminal): void {
export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []): number {
let currentFontName: string | undefined = undefined;
let font: Font | undefined = undefined;
let loadingState: LoadingState = LoadingState.UNLOADED;
let loadError: any | undefined = undefined;
term.registerCharacterJoiner((text: string): [number, number][] => {
return term.registerCharacterJoiner((text: string): [number, number][] => {
// If the font hasn't been loaded yet, load it and return an empty result
const termFont = term.options.fontFamily;
if (
@@ -54,7 +54,7 @@ export function enableLigatures(term: Terminal): void {
// Only refresh things if we actually found a font
if (f) {
term.refresh(0, term.options.rows! - 1);
term.refresh(0, term.rows - 1);
}
}
})
@@ -63,6 +63,9 @@ export function enableLigatures(term: Terminal): void {
// sure our font is still vaild.
if (currentCallFontName === term.options.fontFamily) {
loadingState = LoadingState.FAILED;
if (term.options.logLevel === 'debug') {
console.debug(loadError, new Error('Failure while loading font'));
}
font = undefined;
loadError = e;
}
@@ -76,10 +79,21 @@ export function enableLigatures(term: Terminal): void {
range => [range[0], range[1]]
);
}
if (loadingState === LoadingState.FAILED) {
throw loadError || new Error('Failure while loading font');
}
return [];
return getFallbackRanges(text, fallbackLigatures);
});
}
function getFallbackRanges(text: string, fallbackLigatures: string[]): [number, number][] {
const ranges: [number, number][] = [];
for (let i = 0; i < text.length; i++) {
for (let j = 0; j < fallbackLigatures.length; j++) {
if (text.startsWith(fallbackLigatures[j], i)) {
ranges.push([i, i + fallbackLigatures[j].length]);
i += fallbackLigatures[j].length - 1;
break;
}
}
}
return ranges;
}
@@ -17,11 +17,14 @@ declare module 'xterm-addon-ligatures' {
export class LigaturesAddon implements ITerminalAddon {
/**
* Creates a new ligatures addon.
*
* @param options Options for the ligatures addon.
*/
constructor();
constructor(options?: Partial<ILigatureOptions>);
/**
* Activates the addon
*
* @param terminal The terminal the addon is being loaded in.
*/
public activate(terminal: Terminal): void;
@@ -31,4 +34,25 @@ declare module 'xterm-addon-ligatures' {
*/
public dispose(): void;
}
/**
* Options for the ligatures addon.
*/
export interface ILigatureOptions {
/**
* Fallback ligatures to use when the font access API is either not supported by the browser or
* access is denied. The default set of ligatures is taken from Iosevka's default "calt"
* ligation set: https://typeof.net/Iosevka/
*
* ```
* <-- <--- <<- <- -> ->> --> --->
* <== <=== <<= <= => =>> ==> ===> >= >>=
* <-> <--> <---> <----> <=> <==> <===> <====> -------->
* <~~ <~ ~> ~~> :: ::: == != === !==
* := :- :+ <* <*> *> <| <|> |> +: -: =: :>
* ++ +++ <!-- <!--- <***>
* ```
*/
fallbackLigatures: string[]
}
}
@@ -11,7 +11,6 @@ const mainFile = 'xterm-addon-ligatures.js';
module.exports = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
target: 'electron-renderer',
module: {
rules: [
{
@@ -30,18 +29,19 @@ module.exports = {
},
mode: 'production',
externals: {
'font-finder': 'font-finder',
'fs': 'fs',
'path': 'path',
'stream': 'stream',
'os': 'os',
'util': 'util'
},
resolve: {
// The ligature modules contains fallbacks for node environments, we never want to browserify them
fallback: {
stream: false,
util: false,
fs: false,
os: false,
path: false
path: false,
stream: false,
util: false
}
}
};
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
"version": "0.9.0",
"version": "0.12.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,6 +21,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
+137 -152
View File
@@ -3,8 +3,9 @@
* @license MIT
*/
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
import { EventEmitter } from 'common/EventEmitter';
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
export interface ISearchOptions {
regex?: boolean;
@@ -29,6 +30,10 @@ export interface ISearchPosition {
startRow: number;
}
export interface ISearchAddonOptions {
highlightLimit: number;
}
export interface ISearchResult {
term: string;
col: number;
@@ -47,15 +52,22 @@ type LineCacheEntry = [
lineOffsets: number[]
];
interface IHighlight extends IDisposable {
decoration: IDecoration;
match: ISearchResult;
}
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
export class SearchAddon implements ITerminalAddon {
export class SearchAddon extends Disposable implements ITerminalAddon {
private _terminal: Terminal | undefined;
private _cachedSearchTerm: string | undefined;
private _selectedDecoration: IDecoration | undefined;
private _resultDecorations: Map<number, IDecoration[]> | undefined;
private _searchResults: Map<string, ISearchResult> | undefined;
private _highlightedLines: Set<number> = new Set();
private _highlightDecorations: IHighlight[] = [];
private _selectedDecoration: IHighlight | undefined;
private _highlightLimit: number;
private _onDataDisposable: IDisposable | undefined;
private _onResizeDisposable: IDisposable | undefined;
private _lastSearchOptions: ISearchOptions | undefined;
@@ -70,15 +82,24 @@ export class SearchAddon implements ITerminalAddon {
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
private _resultIndex: number | undefined;
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event;
constructor(options?: Partial<ISearchAddonOptions>) {
super();
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches()));
this.register(toDisposable(() => {
this.clearDecorations();
this._onDataDisposable?.dispose();
this._onResizeDisposable?.dispose();
}));
}
private _updateMatches(): void {
@@ -87,30 +108,18 @@ export class SearchAddon implements ITerminalAddon {
}
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
this._highlightTimeout = setTimeout(() => {
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
const term = this._cachedSearchTerm;
this._cachedSearchTerm = undefined;
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
}, 200);
}
}
public dispose(): void {
this.clearDecorations();
this._onDataDisposable?.dispose();
this._onResizeDisposable?.dispose();
}
public clearDecorations(retainCachedSearchTerm?: boolean): void {
this._selectedDecoration?.dispose();
this._searchResults?.clear();
this._resultDecorations?.forEach(decorations => {
for (const d of decorations) {
d.dispose();
}
});
this._resultDecorations?.clear();
this._searchResults = undefined;
this._resultDecorations = undefined;
this.clearActiveDecoration();
disposeArray(this._highlightDecorations);
this._highlightDecorations = [];
this._highlightedLines.clear();
if (!retainCachedSearchTerm) {
this._cachedSearchTerm = undefined;
}
@@ -126,7 +135,7 @@ export class SearchAddon implements ITerminalAddon {
* doesn't exist, do nothing.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -134,11 +143,16 @@ export class SearchAddon implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
const found = this._findNextAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
@@ -153,32 +167,30 @@ export class SearchAddon implements ITerminalAddon {
// new search, clear out the old decorations
this.clearDecorations(true);
this._searchResults = new Map<string, ISearchResult>();
this._resultDecorations = new Map<number, IDecoration[]>();
const resultDecorations = this._resultDecorations;
const searchResultsWithHighlight: ISearchResult[] = [];
let prevResult: ISearchResult | undefined = undefined;
let result = this._find(term, 0, 0, searchOptions);
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
this._searchResults.set(`${result.row}-${result.col}`, result);
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
if (searchResultsWithHighlight.length >= this._highlightLimit) {
break;
}
prevResult = result;
searchResultsWithHighlight.push(prevResult);
result = this._find(
term,
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
if (this._searchResults.size > 1000) {
this.clearDecorations();
this._resultIndex = undefined;
return;
}
for (const match of searchResultsWithHighlight) {
const decoration = this._createResultDecoration(match, searchOptions.decorations!);
if (decoration) {
this._highlightedLines.add(decoration.marker.line);
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
}
}
this._searchResults.forEach(result => {
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
if (resultDecoration) {
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
decorationsForLine.push(resultDecoration);
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
}
});
}
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
@@ -223,26 +235,22 @@ export class SearchAddon implements ITerminalAddon {
if (!this._terminal || !term || term.length === 0) {
this._terminal?.clearSelection();
this.clearDecorations();
this._cachedSearchTerm = undefined;
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startCol = 0;
let startRow = 0;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false;
// Start from the selection end if there is a selection
// For incremental search, use existing row
currentSelection = this._terminal.getSelectionPosition()!;
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
if (prevSelectedPos) {
if (this._cachedSearchTerm === term) {
startCol = prevSelectedPos.end.x;
startRow = prevSelectedPos.end.y;
} else {
startCol = prevSelectedPos.start.x;
startRow = prevSelectedPos.start.y;
}
}
this._initLinesCache();
@@ -281,24 +289,12 @@ export class SearchAddon implements ITerminalAddon {
}
// If there is only one result, wrap back and return selection if it exists.
if (!result && currentSelection) {
searchPosition.startRow = currentSelection.startRow;
if (!result && prevSelectedPos) {
searchPosition.startRow = prevSelectedPos.start.y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined) {
this._resultIndex = 0;
} else {
this._resultIndex++;
if (this._resultIndex >= this._searchResults.size) {
this._resultIndex = 0;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -307,7 +303,7 @@ export class SearchAddon implements ITerminalAddon {
* doesn't exist, do nothing.
* @param term The search term.
* @param searchOptions Search options.
* @return Whether a result was found.
* @returns Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
@@ -315,75 +311,74 @@ export class SearchAddon implements ITerminalAddon {
}
this._lastSearchOptions = searchOptions;
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
this._highlightAllMatches(term, searchOptions);
}
}
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
const found = this._findPreviousAndSelect(term, searchOptions);
this._fireResults(searchOptions);
this._cachedSearchTerm = term;
return found;
}
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
private _fireResults(searchOptions?: ISearchOptions): void {
if (searchOptions?.decorations) {
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
} else {
this._onDidChangeResults.fire(undefined);
let resultIndex = -1;
if (this._selectedDecoration) {
const selectedMatch = this._selectedDecoration.match;
for (let i = 0; i < this._highlightDecorations.length; i++) {
const match = this._highlightDecorations[i].match;
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
resultIndex = i;
break;
}
}
}
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
}
this._cachedSearchTerm = term;
return found;
}
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
let result: ISearchResult | undefined;
if (!this._terminal || !term || term.length === 0) {
result = undefined;
this._terminal?.clearSelection();
this.clearDecorations();
this._resultIndex = -1;
return false;
}
if (this._cachedSearchTerm !== term) {
this._resultIndex = undefined;
this._terminal.clearSelection();
}
const prevSelectedPos = this._terminal.getSelectionPosition();
this._terminal.clearSelection();
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
let startCol = this._terminal.cols;
const isReverseSearch = true;
const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) {
currentSelection = this._terminal.getSelectionPosition()!;
// Start from selection start if there is a selection
startRow = currentSelection.startRow;
startCol = currentSelection.startColumn;
}
this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
if (incremental) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
if (!isOldResultHighlighted) {
// If selection was not able to be expanded to the right, then try reverse search
if (currentSelection) {
searchPosition.startRow = currentSelection.endRow;
searchPosition.startCol = currentSelection.endColumn;
let result: ISearchResult | undefined;
if (prevSelectedPos) {
searchPosition.startRow = startRow = prevSelectedPos.start.y;
searchPosition.startCol = startCol = prevSelectedPos.start.x;
if (this._cachedSearchTerm !== term) {
// Try to expand selection to right first.
result = this._findInLine(term, searchPosition, searchOptions, false);
if (!result) {
// If selection was not able to be expanded to the right, then try reverse search
searchPosition.startRow = startRow = prevSelectedPos.end.y;
searchPosition.startCol = startCol = prevSelectedPos.end.x;
}
result = this._findInLine(term, searchPosition, searchOptions, true);
}
} else {
}
if (!result) {
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
}
@@ -399,8 +394,8 @@ export class SearchAddon implements ITerminalAddon {
}
}
// If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) {
@@ -409,22 +404,6 @@ export class SearchAddon implements ITerminalAddon {
}
}
if (this._searchResults) {
if (this._searchResults.size === 0) {
this._resultIndex = -1;
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
this._resultIndex = this._searchResults.size - 1;
} else {
this._resultIndex--;
if (this._resultIndex === -1) {
this._resultIndex = this._searchResults.size - 1;
}
}
}
// If there is only one result, return true.
if (!result && currentSelection) return true;
// Set selection and scroll if a result was found
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
}
@@ -477,10 +456,10 @@ export class SearchAddon implements ITerminalAddon {
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched.
* @param term The search term.
* @param position The position to start the search.
* @param searchPosition The position to start the search.
* @param searchOptions Search options.
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
* @return The search result if it was found.
* @returns The search result if it was found.
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
@@ -627,7 +606,7 @@ export class SearchAddon implements ITerminalAddon {
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param lineIndex The index of the line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
@@ -662,7 +641,7 @@ export class SearchAddon implements ITerminalAddon {
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whether a result was selected.
* @returns Whether a result was selected.
*/
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
const terminal = this._terminal!;
@@ -675,7 +654,7 @@ export class SearchAddon implements ITerminalAddon {
if (options) {
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
if (marker) {
this._selectedDecoration = terminal.registerDecoration({
const decoration = terminal.registerDecoration({
marker,
x: result.col,
width: result.size,
@@ -685,8 +664,13 @@ export class SearchAddon implements ITerminalAddon {
color: options.activeMatchColorOverviewRuler
}
});
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
this._selectedDecoration?.onDispose(() => marker.dispose());
if (decoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
}
}
}
@@ -702,16 +686,13 @@ export class SearchAddon implements ITerminalAddon {
}
/**
* Applies styles to the decoration when it is rendered
* @param element the decoration's element
* @param backgroundColor the background color to apply
* @param borderColor the border color to apply
* Applies styles to the decoration when it is rendered.
* @param element The decoration's element.
* @param borderColor The border color to apply.
* @param isActiveResult Whether the element is part of the active search result.
* @returns
*/
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
if (element.clientWidth <= 0) {
return;
}
if (!element.classList.contains('xterm-find-result-decoration')) {
element.classList.add('xterm-find-result-decoration');
if (borderColor) {
@@ -740,13 +721,17 @@ export class SearchAddon implements ITerminalAddon {
x: result.col,
width: result.size,
backgroundColor: options.matchBackground,
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
color: options.matchOverviewRuler,
position: 'center'
}
});
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
findResultDecoration?.onDispose(() => marker.dispose());
if (findResultDecoration) {
const disposables: IDisposable[] = [];
disposables.push(marker);
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
}
return findResultDecoration;
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es6",
+143 -39
View File
@@ -60,35 +60,35 @@ describe('Search Tests', function (): void {
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
await writeSync(page, 'package.json pack package.lock');
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
it('Incremental Find Next', async () => {
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
await writeSync(page, 'package.jsonc');
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
// We look further ahead in the line to ensure that pack was selected from package.lock
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
// We have to reevaluate line because it should have switched starting rows at this point
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
});
it('Simple Regex', async () => {
await writeSync(page, 'abc123defABCD');
@@ -116,10 +116,14 @@ describe('Search Tests', function (): void {
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '𝄞');
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), {
startRow: 0,
endRow: 0,
startColumn: 7,
endColumn: 8
start: {
x: 7,
y: 0
},
end: {
x: 8,
y: 0
}
});
});
@@ -174,7 +178,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
@@ -197,15 +201,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: 0 },
{ resultCount: 1000, resultIndex: 1 },
{ resultCount: 1000, resultIndex: 1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 3, resultIndex: 0 }
]);
});
});
describe('findPrevious', () => {
it('should not fire unless the decorations option is set', async () => {
@@ -229,6 +282,7 @@ describe('Search Tests', function (): void {
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 }
]);
await page.evaluate(`window.term.clearSelection()`);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1, resultIndex: 0 },
@@ -258,7 +312,7 @@ describe('Search Tests', function (): void {
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc aabc');
await writeSync(page, 'd abc aabc d');
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 }
@@ -281,15 +335,64 @@ describe('Search Tests', function (): void {
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 }
]);
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 2 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 2, resultIndex: 0 },
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 0, resultIndex: -1 }
]);
});
it('should fire with more than 1k matches', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
await writeSync(page, data);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 },
{ resultCount: 1000, resultIndex: -1 }
]);
});
it('should fire when writing to terminal', async () => {
await page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 }
]);
await writeSync(page, 'abc bc c\\n\\r');
await timeout(300);
assert.deepStrictEqual(await page.evaluate('window.calls'), [
{ resultCount: 2, resultIndex: 1 },
{ resultCount: 3, resultIndex: 1 }
]);
});
});
});
@@ -313,69 +416,70 @@ describe('Search Tests', function (): void {
await writeSync(page, fixture);
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
});
it('should find all occurrences using findPrevious', async () => {
it('should y all occurrences using findPrevious', async () => {
await writeSync(page, fixture);
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
});
});
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es6",
],
+20 -4
View File
@@ -75,10 +75,28 @@ declare module 'xterm-addon-search' {
activeMatchColorOverviewRuler: string;
}
/**
* Options for the search addon.
*/
export interface ISearchAddonOptions {
/**
* Max number of matches highlighted when decorations are enabled.
* Defaults to 1000 highlighted matches
*/
highlightLimit: number
}
/**
* An xterm.js addon that provides search functionality.
*/
export class SearchAddon implements ITerminalAddon {
/**
* Creates a new search addon.
* @param options Options for the search addon.
*/
constructor(options?: Partial<ISearchAddonOptions>);
/**
* Activates the addon
* @param terminal The terminal the addon is being loaded in.
@@ -121,10 +139,8 @@ declare module 'xterm-addon-search' {
/**
* When decorations are enabled, fires when
* the search results change.
* @returns -1 for resultIndex for a resultCount of 0
* and @returns undefined when the threshold of 1k results
* is exceeded and decorations are disposed of.
* @returns -1 for resultIndex when the threshold of matches is exceeded.
*/
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.7.0",
"version": "0.10.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -24,6 +24,6 @@
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
@@ -7,9 +7,10 @@ import jsdom = require('jsdom');
import { assert } from 'chai';
import { SerializeAddon } from './SerializeAddon';
import { Terminal } from 'browser/public/Terminal';
import { ColorManager } from 'browser/ColorManager';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
import { ThemeService } from 'browser/services/ThemeService';
function sgr(...seq: string[]): string {
return `\x1b[${seq.join(';')}m`;
@@ -44,14 +45,11 @@ class TestSelectionService {
}
describe('xterm-addon-serialize', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: jsdom.DOMWindow;
let serializeAddon: SerializeAddon;
let terminal: Terminal;
let selectionService: any;
before(() => {
serializeAddon = new SerializeAddon();
@@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
@@ -74,13 +71,11 @@ describe('xterm-addon-serialize', () => {
}
});
terminal = new Terminal({ cols: 10, rows: 2 });
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
terminal.loadAddon(serializeAddon);
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
cm = new ColorManager(document, false);
(terminal as any)._core._colorManager = cm;
(terminal as any)._core._selectionService = selectionService;
(terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
});
describe('text', () => {
@@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
&& cell1.isOverline() === cell2.isOverline()
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
&& cell1.isItalic() === cell2.isItalic()
@@ -264,6 +265,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
if (cell.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
@@ -443,8 +445,8 @@ export class SerializeAddon implements ITerminalAddon {
const selection = this._terminal?.getSelectionPosition();
if (selection !== undefined) {
return handler.serialize({
start: { x: selection.startRow, y: selection.startColumn },
end: { x: selection.endRow, y: selection.endColumn }
start: { x: selection.start.y, y: selection.start.x },
end: { x: selection.end.y, y: selection.end.x }
});
}
@@ -544,7 +546,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
this._colors = (_terminal as any)._core._colorManager.colors;
this._colors = (_terminal as any)._core._themeService.colors;
}
private _padStart(target: string, targetLength: number, padString: string): string {
@@ -625,7 +627,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
if (cell.isBold()) { content.push('font-weight: bold;'); }
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
if (cell.isItalic()) { content.push('font-style: italic;'); }
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -42,7 +42,7 @@ describe('SerializeAddon', () => {
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
await page.goto(APP);
await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' });
await openTerminal(page, { rows: 10, cols: 10 });
await page.evaluate(`
window.serializeAddon = new SerializeAddon();
window.term.loadAddon(window.serializeAddon);
@@ -220,6 +220,18 @@ describe('SerializeAddon', () => {
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with overline', async () => {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
sgr(OVERLINED) + line, // Overlined
sgr(UNDERLINED) + line, // Overlined, Underlined
sgr(NORMAL) + line // Normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
@@ -601,6 +613,7 @@ const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
const STRIKETHROUGH = '9';
const OVERLINED = '53';
const NO_BOLD = '22';
const NO_DIM = '22';
@@ -610,3 +623,4 @@ const NO_BLINK = '25';
const NO_INVERSE = '27';
const NO_INVISIBLE = '28';
const NO_STRIKETHROUGH = '29';
const NO_OVERLINED = '55';
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-unicode11",
"version": "0.3.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,6 +21,6 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
@@ -4,7 +4,6 @@
*/
import { IUnicodeVersionProvider } from 'xterm';
import { fill } from 'common/TypedArrayUtils';
type CharWidth = 0 | 1 | 2;
@@ -198,15 +197,15 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
constructor() {
if (!table) {
table = new Uint8Array(65536);
fill(table, 1);
table.fill(1);
table[0] = 0;
fill(table, 0, 1, 32);
fill(table, 0, 0x7f, 0xa0);
table.fill(0, 1, 32);
table.fill(0, 0x7f, 0xa0);
for (let r = 0; r < BMP_COMBINING.length; ++r) {
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
}
for (let r = 0; r < BMP_WIDE.length; ++r) {
fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
table.fill(2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
}
}
}
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
"version": "0.6.0",
"version": "0.8.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -21,6 +21,7 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
import { ILinkProvider, ILink, Terminal, IViewportRange, IBufferLine } from 'xterm';
export interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
@@ -45,55 +45,51 @@ export class LinkComputer {
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
const line = lines.join('');
let match;
let stringIndex = -1;
const result: ILink[] = [];
while ((match = rex.exec(line)) !== null) {
const text = match[1];
if (!text) {
// something matched but does not comply with the given matchIndex
// since this is most likely a bug the regex itself we simply do nothing here
console.log('match found without corresponding matchIndex');
break;
while (match = rex.exec(line)) {
const text = match[0];
// check via URL if the matched text would form a proper url
// NOTE: This outsources the ugly url parsing to the browser.
// To avoid surprising auto expansion from URL we additionally
// check afterwards if the provided string resembles the parsed
// one close enough:
// - decodeURI decode path segement back to byte repr
// to detect unicode auto conversion correctly
// - append / also match domain urls w'o any path notion
try {
const url = new URL(text);
const urlText = decodeURI(url.toString());
if (text !== urlText && text + '/' !== urlText) {
continue;
}
} catch (e) {
continue;
}
// Get index, match.index is for the outer match which includes negated chars
// therefore we cannot use match.index directly, instead we search the position
// of the match group in text again
// also correct regex and string search offsets for the next loop run
stringIndex = line.indexOf(text, stringIndex + 1);
rex.lastIndex = stringIndex + text.length;
if (stringIndex < 0) {
// invalid stringIndex (should not have happened)
break;
}
let endX = stringIndex + text.length;
let endY = startLineIndex + 1;
while (endX > terminal.cols) {
endX -= terminal.cols;
endY++;
}
let startX = stringIndex + 1;
let startY = startLineIndex + 1;
while (startX > terminal.cols) {
startX -= terminal.cols;
startY++;
// map string positions back to buffer positions
// values are 0-based right side excluding
const [startY, startX] = LinkComputer._mapStrIdx(terminal, startLineIndex, 0, match.index);
const [endY, endX] = LinkComputer._mapStrIdx(terminal, startY, startX, text.length);
if (startY === -1 || startX === -1 || endY === -1 || endX === -1) {
continue;
}
// range expects values 1-based right side including, thus +1 except for endX
const range = {
start: {
x: startX,
y: startY
x: startX + 1,
y: startY + 1
},
end: {
x: endX,
y: endY
y: endY + 1
}
};
@@ -104,42 +100,99 @@ export class LinkComputer {
}
/**
* Gets the entire line for the buffer line
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param terminal The terminal
* Get wrapped content lines for the current line index.
* The top/bottom line expansion stops at whitespaces or length > 2048.
* Returns an array with line strings and the top line index.
*
* NOTE: We pull line strings with trimRight=true on purpose to make sure
* to correctly match urls with early wrapped wide chars. This corrupts the string index
* for 1:1 backmapping to buffer positions, thus needs an additional correction in _mapStrIdx.
*/
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
let lineString = '';
let lineWrapsToNext: boolean;
let prevLinesToWrap: boolean;
private static _getWindowedLineStrings(lineIndex: number, terminal: Terminal): [string[], number] {
let line: IBufferLine | undefined;
let topIdx = lineIndex;
let bottomIdx = lineIndex;
let length = 0;
let content = '';
const lines: string[] = [];
do {
const line = terminal.buffer.active.getLine(lineIndex);
if ((line = terminal.buffer.active.getLine(lineIndex))) {
const currentContent = line.translateToString(true);
// expand top, stop on whitespaces or length > 2048
if (line.isWrapped && currentContent[0] !== ' ') {
length = 0;
while ((line = terminal.buffer.active.getLine(--topIdx)) && length < 2048) {
content = line.translateToString(true);
length += content.length;
lines.push(content);
if (!line.isWrapped || content.indexOf(' ') !== -1) {
break;
}
}
lines.reverse();
}
// append current line
lines.push(currentContent);
// expand bottom, stop on whitespaces or length > 2048
length = 0;
while ((line = terminal.buffer.active.getLine(++bottomIdx)) && line.isWrapped && length < 2048) {
content = line.translateToString(true);
length += content.length;
lines.push(content);
if (content.indexOf(' ') !== -1) {
break;
}
}
}
return [lines, topIdx];
}
/**
* Map a string index back to buffer positions.
* Returns buffer position as [lineIndex, columnIndex] 0-based,
* or [-1, -1] in case the lookup ran into a non-existing line.
*/
private static _mapStrIdx(terminal: Terminal, lineIndex: number, rowIndex: number, stringIndex: number): [number, number] {
const buf = terminal.buffer.active;
const cell = buf.getNullCell();
let start = rowIndex;
while (stringIndex) {
const line = buf.getLine(lineIndex);
if (!line) {
break;
return [-1, -1];
}
for (let i = start; i < line.length; ++i) {
line.getCell(i, cell);
const chars = cell.getChars();
const width = cell.getWidth();
if (width) {
stringIndex -= chars.length || 1;
if (line.isWrapped) {
lineIndex--;
// correct stringIndex for early wrapped wide chars:
// - currently only happens at last cell
// - cells to the right are reset with chars='' and width=1 in InputHandler.print
// - follow-up line must be wrapped and contain wide char at first cell
// --> if all these conditions are met, correct stringIndex by +1
if (i === line.length - 1 && chars === '') {
const line = buf.getLine(lineIndex + 1);
if (line && line.isWrapped) {
line.getCell(0, cell);
if (cell.getWidth() === 2) {
stringIndex += 1;
}
}
}
}
if (stringIndex < 0) {
return [lineIndex, i];
}
}
prevLinesToWrap = line.isWrapped;
} while (prevLinesToWrap);
const startLineIndex = lineIndex;
do {
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
break;
}
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
return [lineString, startLineIndex];
start = 0;
}
return [lineIndex, start];
}
}
@@ -3,28 +3,22 @@
* @license MIT
*/
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,18})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+@]*)*([^:"\'\\s])';
const pathClause = '(' + pathCharacterSet + ')?';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
// consider everthing starting with http:// or https://
// up to first whitespace, `"` or `'` as url
// NOTE: The repeated end clause is needed to not match a dangling `:`
// resembling the old (...)*([^:"\'\\s]) final path clause
// additionally exclude early + final:
// - unsafe from rfc3986: !*'()
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
// also exclude as finals:
// - final interpunction like ,.!?
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
// - unsafe chars from rfc1738: {}|\^~[]`
const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
function handleLink(event: MouseEvent, uri: string): void {
const newWindow = window.open();
@@ -41,37 +35,23 @@ function handleLink(event: MouseEvent, uri: string): void {
}
export class WebLinksAddon implements ITerminalAddon {
private _linkMatcherId: number | undefined;
private _terminal: Terminal | undefined;
private _linkProvider: IDisposable | undefined;
constructor(
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
private _options: ILinkMatcherOptions | ILinkProviderOptions = {},
private _useLinkProvider: boolean = false
private _options: ILinkProviderOptions = {}
) {
}
public activate(terminal: Terminal): void {
this._terminal = terminal;
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
const options = this._options as ILinkProviderOptions;
const regex = options.urlRegex || strictUrlRegex;
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
} else {
// TODO: This should be removed eventually
const options = this._options as ILinkMatcherOptions;
options.matchIndex = 1;
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options);
}
const options = this._options as ILinkProviderOptions;
const regex = options.urlRegex || strictUrlRegex;
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
}
public dispose(): void {
if (this._linkMatcherId !== undefined && this._terminal !== undefined) {
this._terminal.deregisterLinkMatcher(this._linkMatcherId);
}
this._linkProvider?.dispose();
}
}
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es2015"
@@ -14,6 +14,22 @@ let page: Page;
const width = 800;
const height = 600;
interface ILinkStateData {
uri?: string;
range?: {
start: {
x: number;
y: number;
};
end: {
x: number;
y: number;
};
};
}
describe('WebLinksAddon', () => {
before(async function(): Promise<any> {
browser = await launchBrowser();
@@ -35,10 +51,47 @@ describe('WebLinksAddon', () => {
it('.io', async function(): Promise<any> {
await testHostName('foo.io');
});
describe('correct buffer offsets & uri', () => {
it('all half width', async () => {
setupCustom();
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
await resetAndHover(5, 1);
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
await resetAndHover(1, 2);
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
});
it('url after full width', async () => {
setupCustom();
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
await resetAndHover(8, 1);
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
await resetAndHover(1, 2);
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
});
it('full width within url and before', async () => {
setupCustom();
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
await resetAndHover(8, 1);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
await resetAndHover(1, 2);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
await resetAndHover(17, 2);
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
});
it('name + password url after full width and combining', async () => {
setupCustom();
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
await resetAndHover(12, 1);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
await resetAndHover(13, 2);
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
});
});
});
async function testHostName(hostname: string): Promise<void> {
await openTerminal(page, { rendererType: 'dom', cols: 40 });
await openTerminal(page, { cols: 40 });
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
const data = ` http://${hostname} \\r\\n` +
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
@@ -64,3 +117,23 @@ async function pollForLinkAtCell(col: number, row: number, value: string): Promi
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
}
async function setupCustom(): Promise<void> {
await openTerminal(page, { cols: 40 });
await page.evaluate(`window._linkStateData = {};
window._linkaddon = new window.WebLinksAddon();
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
window.term.loadAddon(window._linkaddon);`);
}
async function resetAndHover(col: number, row: number): Promise<void> {
await page.evaluate(`window._linkStateData = {};`);
const rowSelector = `.xterm-rows > :nth-child(${row})`;
await page.hover(`${rowSelector} > :nth-child(${col})`);
}
async function evalLinkStateData(uri: string, range: any): Promise<void> {
const data: ILinkStateData = await page.evaluate(`window._linkStateData`);
assert.equal(data.uri, uri);
assert.deepEqual(data.range, range);
}
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es2015"
],
@@ -4,7 +4,7 @@
*/
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm';
import { Terminal, ITerminalAddon, IViewportRange } from 'xterm';
declare module 'xterm-addon-web-links' {
/**
@@ -14,13 +14,9 @@ declare module 'xterm-addon-web-links' {
/**
* Creates a new web links addon.
* @param handler The callback when the link is called.
* @param options Options for the link matcher.
* @param useLinkProvider Whether to use the new link provider API to create
* the links. This is an option because use of both link matcher (old) and
* link provider (new) may cause issues. Link provider will eventually be
* the default and only option.
* @param options Options for the link provider.
*/
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean);
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions);
/**
* Activates the addon
@@ -39,8 +35,7 @@ declare module 'xterm-addon-web-links' {
*/
export interface ILinkProviderOptions {
/**
* A callback that fires when the mouse hovers over a link for a period of
* time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}).
* A callback that fires when the mouse hovers over a link.
*/
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
@@ -50,7 +45,7 @@ declare module 'xterm-addon-web-links' {
*/
leave?(event: MouseEvent, text: string): void;
/**
/**
* A callback to use instead of the default one.
*/
urlRegex?: RegExp;
+5
View File
@@ -15,6 +15,7 @@ import { Terminal } from 'xterm';
import { WebglAddon } from 'xterm-addon-webgl';
const terminal = new Terminal();
terminal.open(element);
terminal.loadAddon(new WebglAddon());
```
@@ -34,3 +35,7 @@ terminal.loadAddon(addon);
```
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
### See also
- [xterm-addon-canvas](https://www.npmjs.com/package/xterm-addon-canvas) A renderer for xterm.js that uses a 2d canvas that can be used as a fallback when WebGL is not available
+3 -2
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.12.0",
"version": "0.15.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -22,6 +22,7 @@
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
"xterm": "^5.0.0"
}
}
+171 -102
View File
@@ -3,14 +3,14 @@
* @license MIT
*/
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { Terminal } from 'xterm';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
interface IVertices {
attributes: Float32Array;
@@ -29,8 +29,9 @@ const enum VertexAttribLocations {
CELL_POSITION = 1,
OFFSET = 2,
SIZE = 3,
TEXCOORD = 4,
TEXSIZE = 5
TEXPAGE = 4,
TEXCOORD = 5,
TEXSIZE = 6
}
const vertexShaderSource = `#version 300 es
@@ -38,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
@@ -45,43 +47,59 @@ uniform mat4 u_projection;
uniform vec2 u_resolution;
out vec2 v_texcoord;
flat out int v_texpage;
void main() {
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_texpage = int(a_texpage);
v_texcoord = a_texcoord + a_unitquad * a_texsize;
}`;
const fragmentShaderSource = `#version 300 es
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
let textureConditionals = '';
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
}
return (`#version 300 es
precision lowp float;
in vec2 v_texcoord;
flat in int v_texpage;
uniform sampler2D u_texture;
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
out vec4 outColor;
void main() {
outColor = texture(u_texture, v_texcoord);
}`;
if (v_texpage == 0) {
outColor = texture(u_texture[0], v_texcoord);
} ${textureConditionals}
}`);
}
const INDICES_PER_CELL = 10;
const INDICES_PER_CELL = 11;
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
const CELL_POSITION_INDICES = 2;
export class GlyphRenderer {
private _atlas: WebglCharAtlas | undefined;
// Work variables to avoid garbage collection
let $i = 0;
let $glyph: IRasterizedGlyph | undefined = undefined;
let $leftCellPadding = 0;
let $clippedPixels = 0;
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
private _projectionLocation: WebGLUniformLocation;
private _resolutionLocation: WebGLUniformLocation;
private _textureLocation: WebGLUniformLocation;
private _atlasTexture: WebGLTexture;
private _attributesBuffer: WebGLBuffer;
export class GlyphRenderer extends Disposable {
private readonly _program: WebGLProgram;
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
private readonly _projectionLocation: WebGLUniformLocation;
private readonly _resolutionLocation: WebGLUniformLocation;
private readonly _textureLocation: WebGLUniformLocation;
private readonly _atlasTextures: GLTexture[];
private readonly _attributesBuffer: WebGLBuffer;
private _atlas: ITextureAtlas | undefined;
private _activeBuffer: number = 0;
private _vertices: IVertices = {
private readonly _vertices: IVertices = {
count: 0,
attributes: new Float32Array(0),
attributesBuffers: [
@@ -91,14 +109,23 @@ export class GlyphRenderer {
};
constructor(
private _terminal: Terminal,
private _colors: IColorSet,
private _gl: IWebGL2RenderingContext,
private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
) {
super();
const gl = this._gl;
const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
this._program = program;
if (TextureAtlas.maxAtlasPages === undefined) {
// Typically 8 or 16
TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
// Almost all clients will support >= 4096
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
}
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
@@ -112,20 +139,24 @@ export class GlyphRenderer {
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const unitQuadVerticesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
// Setup attributes
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
@@ -133,118 +164,155 @@ export class GlyphRenderer {
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
// Setup empty texture atlas
this._atlasTexture = throwIfFalsy(gl.createTexture());
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
// Setup static uniforms
gl.useProgram(this._program);
const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
textureUnits[i] = i;
}
gl.uniform1iv(this._textureLocation, textureUnits);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
// is ever drawn it will show characters as red rectangles.
this._atlasTextures = [];
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
this._atlasTextures[i] = glTexture;
}
// Allow drawing of transparent texture
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
// Set viewport
this.onResize();
this.handleResize();
}
public beginFrame(): boolean {
return this._atlas ? this._atlas.beginFrame() : true;
}
public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void {
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars);
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
// should not instantiate any variables unless a new glyph is drawn to the cache where the
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph.
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
}
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void {
const terminal = this._terminal;
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
// Exit early if this is a null character, allow space character to continue as it may have
// underline/strikethrough styles
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
}
let rasterizedGlyph: IRasterizedGlyph;
if (!this._atlas) {
return;
}
// Get the glyph
if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
} else {
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
}
// Fill empty if no glyph was found
if (!rasterizedGlyph) {
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
return;
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
$clippedPixels = $glyph.offset.x - $leftCellPadding;
// a_origin
array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
array[$i + 8] = $glyph.sizeClipSpace.y;
} else {
// a_origin
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
// a_size
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
// a_texpage
array[$i + 4] = $glyph.texturePage;
// a_texcoord
array[$i + 5] = $glyph.texturePositionClipSpace.x;
array[$i + 6] = $glyph.texturePositionClipSpace.y;
// a_texsize
array[$i + 7] = $glyph.sizeClipSpace.x;
array[$i + 8] = $glyph.sizeClipSpace.y;
}
// a_origin
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
// a_size
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
// a_texcoord
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
// a_texsize
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
// a_cellpos only changes on resize
}
public clear(force?: boolean): void {
public clear(): void {
const terminal = this._terminal;
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
// Don't clear if not forced and the array length is correct
if (!force && this._vertices.count === newCount) {
return;
}
// Clear vertices
this._vertices.count = newCount;
this._vertices.attributes = new Float32Array(newCount);
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
if (this._vertices.count !== newCount) {
this._vertices.attributes = new Float32Array(newCount);
} else {
this._vertices.attributes.fill(0);
}
let i = 0;
for (; i < this._vertices.attributesBuffers.length; i++) {
if (this._vertices.count !== newCount) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
} else {
this._vertices.attributesBuffers[i].fill(0);
}
}
this._vertices.count = newCount;
i = 0;
for (let y = 0; y < terminal.rows; y++) {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 8] = x / terminal.cols;
this._vertices.attributes[i + 9] = y / terminal.rows;
this._vertices.attributes[i + 9] = x / terminal.cols;
this._vertices.attributes[i + 10] = y / terminal.rows;
i += INDICES_PER_CELL;
}
}
}
public onResize(): void {
public handleResize(): void {
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
this.clear();
}
public setColors(): void {
}
public render(renderModel: IRenderModel): void {
if (!this._atlas) {
return;
@@ -278,31 +346,32 @@ export class GlyphRenderer {
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
// Bind the texture atlas if it's changed
if (this._atlas.hasCanvasChanged) {
this._atlas.hasCanvasChanged = false;
gl.uniform1i(this._textureLocation, 0);
gl.activeTexture(gl.TEXTURE0 + 0);
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
gl.generateMipmap(gl.TEXTURE_2D);
// Bind the atlas page texture if they have changed
for (let i = 0; i < this._atlas.pages.length; i++) {
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
this._bindAtlasPageTexture(gl, this._atlas, i);
}
}
// Set uniforms
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
// Draw the viewport
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
}
public setAtlas(atlas: WebglCharAtlas): void {
const gl = this._gl;
public setAtlas(atlas: ITextureAtlas): void {
this._atlas = atlas;
for (const glTexture of this._atlasTextures) {
glTexture.version = -1;
}
}
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
gl.activeTexture(gl.TEXTURE0 + i);
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
gl.generateMipmap(gl.TEXTURE_2D);
this._atlasTextures[i].version = atlas.pages[i].version;
}
public setDimensions(dimensions: IRenderDimensions): void {
@@ -3,14 +3,18 @@
* @license MIT
*/
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
import { Attributes, FgFlags } from 'common/buffer/Constants';
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
import { Terminal } from 'xterm';
import { IColor } from 'common/Types';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { IThemeService } from 'browser/services/Services';
const enum VertexAttribLocations {
POSITION = 0,
@@ -26,12 +30,11 @@ layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
uniform mat4 u_projection;
uniform vec2 u_resolution;
out vec4 v_color;
void main() {
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
vec2 zeroToOne = a_position + (a_unitquad * a_size);
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
v_color = a_color;
}`;
@@ -57,11 +60,20 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
export class RectangleRenderer {
// Work variables to avoid garbage collection
let $rgba = 0;
let $isDefault = false;
let $x1 = 0;
let $y1 = 0;
let $r = 0;
let $g = 0;
let $b = 0;
let $a = 0;
export class RectangleRenderer extends Disposable {
private _program: WebGLProgram;
private _vertexArrayObject: IWebGLVertexArrayObject;
private _resolutionLocation: WebGLUniformLocation;
private _attributesBuffer: WebGLBuffer;
private _projectionLocation: WebGLUniformLocation;
private _bgFloat!: Float32Array;
@@ -73,16 +85,18 @@ export class RectangleRenderer {
constructor(
private _terminal: Terminal,
private _colors: IColorSet,
private _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions
private _dimensions: IRenderDimensions,
private readonly _themeService: IThemeService
) {
super();
const gl = this._gl;
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
this.register(toDisposable(() => gl.deleteProgram(this._program)));
// Uniform locations
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
// Create and set the vertex array object
@@ -92,20 +106,24 @@ export class RectangleRenderer {
// Setup a_unitquad, this defines the 4 vertices of a rectangle
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
const unitQuadVerticesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
// Setup the unit quad element array buffer, this points to indices in
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
// triangle strip
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
const elementIndicesBuffer = gl.createBuffer();
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
// Setup attributes
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.enableVertexAttribArray(VertexAttribLocations.POSITION);
gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0);
@@ -117,7 +135,11 @@ export class RectangleRenderer {
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
this._updateCachedColors();
this._updateCachedColors(_themeService.colors);
this.register(this._themeService.onChangeColors(e => {
this._updateCachedColors(e);
this._updateViewportRectangle();
}));
}
public render(): void {
@@ -128,25 +150,23 @@ export class RectangleRenderer {
gl.bindVertexArray(this._vertexArrayObject);
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
// Bind attributes buffer and draw
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
}
public onResize(): void {
public handleResize(): void {
this._updateViewportRectangle();
}
public setColors(): void {
this._updateCachedColors();
this._updateViewportRectangle();
public setDimensions(dimensions: IRenderDimensions): void {
this._dimensions = dimensions;
}
private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background);
private _updateCachedColors(colors: ReadonlyColorSet): void {
this._bgFloat = this._colorToFloat32Array(colors.background);
}
private _updateViewportRectangle(): void {
@@ -156,8 +176,8 @@ export class RectangleRenderer {
0,
0,
0,
this._terminal.cols * this._dimensions.scaledCellWidth,
this._terminal.rows * this._dimensions.scaledCellHeight,
this._terminal.cols * this._dimensions.device.cell.width,
this._terminal.rows * this._dimensions.device.cell.height,
this._bgFloat
);
}
@@ -166,22 +186,34 @@ export class RectangleRenderer {
const terminal = this._terminal;
const vertices = this._vertices;
// Declare variable ahead of time to avoid garbage collection
let rectangleCount = 1;
let y: number;
let x: number;
let currentStartX: number;
let currentBg: number;
let currentFg: number;
let currentInverse: boolean;
let modelIndex: number;
let bg: number;
let fg: number;
let inverse: boolean;
let offset: number;
for (let y = 0; y < terminal.rows; y++) {
let currentStartX = -1;
let currentBg = 0;
let currentFg = 0;
let currentInverse = false;
for (let x = 0; x < terminal.cols; x++) {
const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
const inverse = !!(fg & FgFlags.INVERSE);
for (y = 0; y < terminal.rows; y++) {
currentStartX = -1;
currentBg = 0;
currentFg = 0;
currentInverse = false;
for (x = 0; x < terminal.cols; x++) {
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
inverse = !!(fg & FgFlags.INVERSE);
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
// A rectangle needs to be drawn if going from non-default to another color
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
}
currentStartX = x;
@@ -192,7 +224,7 @@ export class RectangleRenderer {
}
// Finish rectangle if it's still going
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
}
}
@@ -200,52 +232,54 @@ export class RectangleRenderer {
}
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
let rgba: number | undefined;
$isDefault = false;
if (fg & FgFlags.INVERSE) {
switch (fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
$rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (fg & Attributes.RGB_MASK) << 8;
$rgba = (fg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.foreground.rgba;
$rgba = this._themeService.colors.foreground.rgba;
}
} else {
switch (bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
$rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (bg & Attributes.RGB_MASK) << 8;
$rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.background.rgba;
$rgba = this._themeService.colors.background.rgba;
$isDefault = true;
}
}
if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
}
const x1 = startX * this._dimensions.scaledCellWidth;
const y1 = y * this._dimensions.scaledCellHeight;
const r = ((rgba >> 24) & 0xFF) / 255;
const g = ((rgba >> 16) & 0xFF) / 255;
const b = ((rgba >> 8 ) & 0xFF) / 255;
$x1 = startX * this._dimensions.device.cell.width;
$y1 = y * this._dimensions.device.cell.height;
$r = (($rgba >> 24) & 0xFF) / 255;
$g = (($rgba >> 16) & 0xFF) / 255;
$b = (($rgba >> 8 ) & 0xFF) / 255;
$a = 1;
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a);
}
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
array[offset ] = x1;
array[offset + 1] = y1;
array[offset + 2] = width;
array[offset + 3] = height;
array[offset ] = x1 / this._dimensions.device.canvas.width;
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
array[offset + 2] = width / this._dimensions.device.canvas.width;
array[offset + 3] = height / this._dimensions.device.canvas.height;
array[offset + 4] = r;
array[offset + 5] = g;
array[offset + 6] = b;
@@ -253,10 +287,10 @@ export class RectangleRenderer {
}
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
array[offset ] = x1;
array[offset + 1] = y1;
array[offset + 2] = width;
array[offset + 3] = height;
array[offset ] = x1 / this._dimensions.device.canvas.width;
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
array[offset + 2] = width / this._dimensions.device.canvas.width;
array[offset + 3] = height / this._dimensions.device.canvas.height;
array[offset + 4] = color[0];
array[offset + 5] = color[1];
array[offset + 6] = color[2];
+8 -25
View File
@@ -3,12 +3,14 @@
* @license MIT
*/
import { IRenderModel, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils';
import { IRenderModel } from './Types';
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
export const RENDER_MODEL_INDICIES_PER_CELL = 3;
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
export const RENDER_MODEL_BG_OFFSET = 1;
export const RENDER_MODEL_FG_OFFSET = 2;
export const RENDER_MODEL_EXT_OFFSET = 3;
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
@@ -20,16 +22,7 @@ export class RenderModel implements IRenderModel {
constructor() {
this.cells = new Uint32Array(0);
this.lineLengths = new Uint32Array(0);
this.selection = {
hasSelection: false,
columnSelectMode: false,
viewportStartRow: 0,
viewportEndRow: 0,
viewportCappedStartRow: 0,
viewportCappedEndRow: 0,
startCol: 0,
endCol: 0
};
this.selection = createSelectionRenderModel();
}
public resize(cols: number, rows: number): void {
@@ -41,17 +34,7 @@ export class RenderModel implements IRenderModel {
}
public clear(): void {
fill(this.cells, 0, 0);
fill(this.lineLengths, 0, 0);
}
public clearSelection(): void {
this.selection.hasSelection = false;
this.selection.viewportStartRow = 0;
this.selection.viewportEndRow = 0;
this.selection.viewportCappedStartRow = 0;
this.selection.viewportCappedEndRow = 0;
this.selection.startCol = 0;
this.selection.endCol = 0;
this.cells.fill(0, 0);
this.lineLengths.fill(0, 0);
}
}
+1 -55
View File
@@ -3,50 +3,7 @@
* @license MIT
*/
export interface IRasterizedGlyphSet {
[bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
}
/**
* Represents a rasterized glyph within a texture atlas. Some numbers are
* tracked in CSS pixels as well in order to reduce calculations during the
* render loop.
*/
export interface IRasterizedGlyph {
/**
* The x and y offset between the glyph's top/left and the top/left of a cell
* in pixels.
*/
offset: IVector;
/**
* the x and y position of the glyph in the texture in pixels.
*/
texturePosition: IVector;
/**
* the x and y position of the glyph in the texture in clip space coordinates.
*/
texturePositionClipSpace: IVector;
/**
* The width and height of the glyph in the texture in pixels.
*/
size: IVector;
/**
* The width and height of the glyph in the texture in clip space coordinates.
*/
sizeClipSpace: IVector;
}
export interface IVector {
x: number;
y: number;
}
export interface IBoundingBox {
top: number;
left: number;
right: number;
bottom: number;
}
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
export interface IRenderModel {
cells: Uint32Array;
@@ -54,17 +11,6 @@ export interface IRenderModel {
selection: ISelectionRenderModel;
}
export interface ISelectionRenderModel {
hasSelection: boolean;
columnSelectMode: boolean;
viewportStartRow: number;
viewportEndRow: number;
viewportCappedStartRow: number;
viewportCappedEndRow: number;
startCol: number;
endCol: number;
}
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
vertexAttribDivisor(index: number, divisor: number): void;
createVertexArray(): IWebGLVertexArrayObject;
+62 -33
View File
@@ -3,49 +3,78 @@
* @license MIT
*/
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { getSafariVersion, isSafari } from 'common/Platform';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { ITerminalAddon, Terminal } from 'xterm';
import { WebglRenderer } from './WebglRenderer';
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { isSafari } from 'common/Platform';
import { IDecorationService } from 'common/services/Services';
export class WebglAddon implements ITerminalAddon {
export class WebglAddon extends Disposable implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
private readonly _onContextLoss = this.register(new EventEmitter<void>());
public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _preserveDrawingBuffer?: boolean
) {}
public activate(terminal: Terminal): void {
if (!terminal.element) {
throw new Error('Cannot activate WebglAddon before Terminal.open');
) {
if (isSafari && getSafariVersion() < 16) {
throw new Error('Webgl2 is only supported on Safari 16 and above');
}
if (isSafari) {
throw new Error('Webgl is not currently supported on Safari');
}
this._terminal = terminal;
const renderService: IRenderService = (terminal as any)._core._renderService;
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer);
this._renderer.onContextLoss(() => this._onContextLoss.fire());
renderService.setRenderer(this._renderer);
super();
}
public dispose(): void {
if (!this._terminal) {
throw new Error('Cannot dispose WebglAddon because it is activated');
public activate(terminal: Terminal): void {
const core = (terminal as any)._core as ITerminal;
if (!terminal.element) {
this.register(core.onWillOpen(() => this.activate(terminal)));
return;
}
const renderService: IRenderService = (this._terminal as any)._core._renderService;
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.onResize(this._terminal.cols, this._terminal.rows);
this._renderer = undefined;
this._terminal = terminal;
const coreService: ICoreService = core.coreService;
const optionsService: IOptionsService = core.optionsService;
const unsafeCore = core as any;
const renderService: IRenderService = unsafeCore._renderService;
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
const decorationService: IDecorationService = unsafeCore._decorationService;
const themeService: IThemeService = unsafeCore._themeService;
this._renderer = this.register(new WebglRenderer(
terminal,
characterJoinerService,
charSizeService,
coreBrowserService,
coreService,
decorationService,
optionsService,
themeService,
this._preserveDrawingBuffer
));
this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
this.register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
renderService.setRenderer(this._renderer);
this.register(toDisposable(() => {
const renderService: IRenderService = (this._terminal as any)._core._renderService;
renderService.setRenderer((this._terminal as any)._core._createRenderer());
renderService.handleResize(terminal.cols, terminal.rows);
}));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
@@ -53,6 +82,6 @@ export class WebglAddon implements ITerminalAddon {
}
public clearTextureAtlas(): void {
this._renderer?.clearCharAtlas();
this._renderer?.clearTextureAtlas();
}
}
+296 -339
View File
@@ -3,84 +3,90 @@
* @license MIT
*/
import { GlyphRenderer } from './GlyphRenderer';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { Disposable } from 'common/Lifecycle';
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal, IEvent } from 'xterm';
import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
import { ITerminal, IColorSet } from 'browser/Types';
import { EventEmitter } from 'common/EventEmitter';
import { CellData } from 'common/buffer/CellData';
import { addDisposableDomListener } from 'browser/Lifecycle';
import { ICharacterJoinerService } from 'browser/services/Services';
import { CharData, ICellData } from 'common/Types';
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ITerminal } from 'browser/Types';
import { AttributeData } from 'common/buffer/AttributeData';
import { IDecorationService } from 'common/services/Services';
import { CellData } from 'common/buffer/CellData';
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { IDisposable, Terminal } from 'xterm';
import { GlyphRenderer } from './GlyphRenderer';
import { RectangleRenderer } from './RectangleRenderer';
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
import { IRenderLayer } from './renderLayer/Types';
import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { IWebGL2RenderingContext } from './Types';
export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[];
private _charAtlas: WebglCharAtlas | undefined;
private _charAtlasDisposable: IDisposable | undefined;
private _charAtlas: ITextureAtlas | undefined;
private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 };
private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext;
private _rectangleRenderer: RectangleRenderer;
private _glyphRenderer: GlyphRenderer;
private _rectangleRenderer?: RectangleRenderer;
private _glyphRenderer?: GlyphRenderer;
public dimensions: IRenderDimensions;
public readonly dimensions: IRenderDimensions;
private _core: ITerminal;
private _isAttached: boolean;
private _contextRestorationTimeout: number | undefined;
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
private _onContextLoss = new EventEmitter<void>();
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
public readonly onRequestRedraw = this._onRequestRedraw.event;
private readonly _onContextLoss = this.register(new EventEmitter<void>());
public readonly onContextLoss = this._onContextLoss.event;
constructor(
private _terminal: Terminal,
private _colors: IColorSet,
private readonly _characterJoinerService: ICharacterJoinerService,
private readonly _charSizeService: ICharSizeService,
private readonly _coreBrowserService: ICoreBrowserService,
coreService: ICoreService,
private readonly _decorationService: IDecorationService,
private readonly _optionsService: IOptionsService,
private readonly _themeService: IThemeService,
preserveDrawingBuffer?: boolean
) {
super();
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
this._core = (this._terminal as any)._core;
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService),
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, _optionsService, this._themeService)
];
this.dimensions = {
scaledCharWidth: 0,
scaledCharHeight: 0,
scaledCellWidth: 0,
scaledCellHeight: 0,
scaledCharLeft: 0,
scaledCharTop: 0,
scaledCanvasWidth: 0,
scaledCanvasHeight: 0,
canvasWidth: 0,
canvasHeight: 0,
actualCellWidth: 0,
actualCellHeight: 0
};
this._devicePixelRatio = window.devicePixelRatio;
this.dimensions = createRenderDimensions();
this._devicePixelRatio = this._coreBrowserService.dpr;
this._updateDimensions();
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
this._canvas = document.createElement('canvas');
@@ -94,58 +100,67 @@ export class WebglRenderer extends Disposable implements IRenderer {
throw new Error('WebGL2 not supported ' + this._gl);
}
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
console.log('webglcontextlost event received');
// Prevent the default behavior in order to enable WebGL context restoration.
e.preventDefault();
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
// If not, dispatch the onContextLoss notification to observers.
this._contextRestorationTimeout = setTimeout(() => {
this._contextRestorationTimeout = undefined;
console.warn('webgl context not restored; firing onContextLoss');
this._onContextLoss.fire(e);
}, 3000 /* ms */);
}));
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
console.warn('webglcontextrestored event received');
clearTimeout(this._contextRestorationTimeout);
this._contextRestorationTimeout = undefined;
// The texture atlas and glyph renderer must be fully reinitialized
// because their contents have been lost.
removeTerminalFromCache(this._terminal);
this._initializeWebGLState();
this._requestRedrawViewport();
}));
this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
this._core.screenElement!.appendChild(this._canvas);
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
[this._rectangleRenderer, this._glyphRenderer] = this._initializeWebGLState();
// Update dimensions and acquire char atlas
this.onCharSizeChanged();
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
this._isAttached = document.body.contains(this._core.screenElement!);
}
public dispose(): void {
for (const l of this._renderLayers) {
l.dispose();
}
this._canvas.parentElement?.removeChild(this._canvas);
super.dispose();
this.register(toDisposable(() => {
for (const l of this._renderLayers) {
l.dispose();
}
this._canvas.parentElement?.removeChild(this._canvas);
removeTerminalFromCache(this._terminal);
}));
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._charAtlas?.cacheCanvas;
return this._charAtlas?.pages[0].canvas;
}
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
for (const l of this._renderLayers) {
l.setColors(this._terminal, this._colors);
l.reset(this._terminal);
}
this._rectangleRenderer.setColors();
this._glyphRenderer.setColors();
private _handleColorChange(): void {
this._refreshCharAtlas();
// Force a full refresh
this._model.clear();
this._clearModel(true);
}
public onDevicePixelRatioChange(): void {
public handleDevicePixelRatioChange(): void {
// If the device pixel ratio changed, the char atlas needs to be regenerated
// and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows);
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
this._devicePixelRatio = this._coreBrowserService.dpr;
this.handleResize(this._terminal.cols, this._terminal.rows);
}
}
public onResize(cols: number, rows: number): void {
public handleResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
@@ -157,94 +172,136 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
// Resize the canvas
this._canvas.width = this.dimensions.scaledCanvasWidth;
this._canvas.height = this.dimensions.scaledCanvasHeight;
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
this._canvas.width = this.dimensions.device.canvas.width;
this._canvas.height = this.dimensions.device.canvas.height;
this._canvas.style.width = `${this.dimensions.css.canvas.width}px`;
this._canvas.style.height = `${this.dimensions.css.canvas.height}px`;
// Resize the screen
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
this._rectangleRenderer.onResize();
this._glyphRenderer.setDimensions(this.dimensions);
this._glyphRenderer.onResize();
this._rectangleRenderer?.setDimensions(this.dimensions);
this._rectangleRenderer?.handleResize();
this._glyphRenderer?.setDimensions(this.dimensions);
this._glyphRenderer?.handleResize();
this._refreshCharAtlas();
// Force a full refresh
this._model.clear();
// Force a full refresh. Resizing `_glyphRenderer` should clear it already,
// so there is no need to clear it again here.
this._clearModel(false);
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows);
public handleCharSizeChanged(): void {
this.handleResize(this._terminal.cols, this._terminal.rows);
}
public onBlur(): void {
public handleBlur(): void {
for (const l of this._renderLayers) {
l.onBlur(this._terminal);
l.handleBlur(this._terminal);
}
}
public onFocus(): void {
for (const l of this._renderLayers) {
l.onFocus(this._terminal);
}
}
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
for (const l of this._renderLayers) {
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
}
this._updateSelectionModel(start, end, columnSelectMode);
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
public onCursorMove(): void {
public handleFocus(): void {
for (const l of this._renderLayers) {
l.onCursorMove(this._terminal);
l.handleFocus(this._terminal);
}
// Request a redraw for active/inactive selection background
this._requestRedrawViewport();
}
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
for (const l of this._renderLayers) {
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
}
this._model.selection.update(this._terminal, start, end, columnSelectMode);
this._requestRedrawViewport();
}
public handleCursorMove(): void {
for (const l of this._renderLayers) {
l.handleCursorMove(this._terminal);
}
}
public onOptionsChanged(): void {
for (const l of this._renderLayers) {
l.onOptionsChanged(this._terminal);
}
private _handleOptionsChanged(): void {
this._updateDimensions();
this._refreshCharAtlas();
}
/**
* Initializes members dependent on WebGL context state.
*/
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
// Dispose any previous rectangle and glyph renderers before creating new ones.
this._rectangleRenderer?.dispose();
this._glyphRenderer?.dispose();
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService));
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions));
// Update dimensions and acquire char atlas
this.handleCharSizeChanged();
return [this._rectangleRenderer, this._glyphRenderer];
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(): void {
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) {
// Mark as not attached so char atlas gets refreshed on next render
this._isAttached = false;
return;
}
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
if (!('getRasterizedGlyph' in atlas)) {
throw new Error('The webgl renderer only works with the webgl char atlas');
const atlas = acquireTextureAtlas(
this._terminal,
this._optionsService.rawOptions,
this._themeService.colors,
this.dimensions.device.cell.width,
this.dimensions.device.cell.height,
this.dimensions.device.char.width,
this.dimensions.device.char.height,
this._coreBrowserService.dpr
);
if (this._charAtlas !== atlas) {
this._charAtlasDisposable?.dispose();
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
this._charAtlasDisposable = getDisposeArrayDisposable([
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
]);
}
this._charAtlas = atlas as WebglCharAtlas;
this._charAtlas = atlas;
this._charAtlas.warmUp();
this._glyphRenderer.setAtlas(this._charAtlas);
this._glyphRenderer?.setAtlas(this._charAtlas);
}
public clearCharAtlas(): void {
this._charAtlas?.clearTexture();
/**
* Clear the model.
* @param clearGlyphRenderer Whether to also clear the glyph renderer. This
* should be true generally to make sure it is in the same state as the model.
*/
private _clearModel(clearGlyphRenderer: boolean): void {
this._model.clear();
this._updateModel(0, this._terminal.rows - 1);
if (clearGlyphRenderer) {
this._glyphRenderer?.clear();
}
}
public clearTextureAtlas(): void {
this._charAtlas?.clearTexture();
this._clearModel(true);
this._requestRedrawViewport();
}
public clear(): void {
this._model.clear();
this._glyphRenderer.clear(true);
this._clearModel(true);
for (const l of this._renderLayers) {
l.reset(this._terminal);
}
@@ -260,7 +317,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) {
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
@@ -271,48 +328,75 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Update render layers
for (const l of this._renderLayers) {
l.onGridChanged(this._terminal, start, end);
l.handleGridChanged(this._terminal, start, end);
}
if (!this._glyphRenderer || !this._rectangleRenderer) {
return;
}
// Tell renderer the frame is beginning
// upon a model clear also refresh the full viewport model
// (also triggered by an atlas page merge, part of #4480)
if (this._glyphRenderer.beginFrame()) {
this._model.clear();
this._updateSelectionModel(undefined, undefined);
this._clearModel(true);
this._updateModel(0, this._terminal.rows - 1);
} else {
// just update changed lines to draw
this._updateModel(start, end);
}
// Update model to reflect what's drawn
this._updateModel(start, end);
// Render
this._rectangleRenderer.render();
this._glyphRenderer.render(this._model);
this._rectangleRenderer?.render();
this._glyphRenderer?.render(this._model);
}
private _updateModel(start: number, end: number): void {
const terminal = this._core;
let cell: ICellData = this._workCell;
for (let y = start; y <= end; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row)!;
// Declare variable ahead of time to avoid garbage collection
let lastBg: number;
let y: number;
let row: number;
let line: IBufferLine;
let joinedRanges: [number, number][];
let isJoined: boolean;
let lastCharX: number;
let range: [number, number];
let chars: string;
let code: number;
let i: number;
let x: number;
let j: number;
start = clamp(start, terminal.rows - 1, 0);
end = clamp(end, terminal.rows - 1, 0);
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0;
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (let x = 0; x < terminal.cols; x++) {
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
for (x = 0; x < terminal.cols; x++) {
lastBg = this._cellColorResolver.result.bg;
line.loadCell(x, cell);
if (x === 0) {
lastBg = this._cellColorResolver.result.bg;
}
// If true, indicates that the current character(s) to draw were joined.
let isJoined = false;
let lastCharX = x;
isJoined = false;
lastCharX = x;
// Process any joined character ranges as needed. Because of how the
// ranges are produced, we know that they are valid for the characters
// and attributes of our input.
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
isJoined = true;
const range = joinedRanges.shift()!;
range = joinedRanges.shift()!;
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
// so we get the string and width representing it directly.
cell = new JoinedCellData(
cell,
line!.translateToString(true, range[0], range[1]),
@@ -323,12 +407,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
lastCharX = range[1] - 1;
}
const chars = cell.getChars();
let code = cell.getCode();
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
chars = cell.getChars();
code = cell.getCode();
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Load colors/resolve overrides into work colors
this._loadColorsForCell(x, row);
this._cellColorResolver.resolve(cell, x, row);
if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1;
@@ -336,8 +420,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Nothing has changed, no updates needed
if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
continue;
}
@@ -348,10 +433,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Cache the results in the model
this._model.cells[i] = code;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars);
this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
if (isJoined) {
// Restore work cell
@@ -359,216 +445,83 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x < lastCharX; x++) {
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
this._model.cells[j] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
}
}
}
}
this._rectangleRenderer.updateBackgrounds(this._model);
}
/**
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
* necessary which is why the work cell object is not used.
*/
private _loadColorsForCell(x: number, y: number): void {
this._workColors.bg = this._workCell.bg;
this._workColors.fg = this._workCell.fg;
// Get any foreground/background overrides, this happens on the model to avoid spreading
// override logic throughout the different sub-renderers
let bgOverride: number | undefined;
let fgOverride: number | undefined;
// Apply decorations on the bottom layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
}
// Apply the selection color if needed
if (this._isCellSelected(x, y)) {
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
if (this._colors.selectionForeground) {
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
}
}
// Apply decorations on the top layer
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
if (d.backgroundColorRGB) {
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
if (d.foregroundColorRGB) {
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
}
}
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
// ahead of time in order to use the correct cache key
if (bgOverride !== undefined) {
// Non-RGB attributes from model + override + force RGB color mode
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
}
if (fgOverride !== undefined) {
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
}
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
// resolving the other inverse color and setting the inverse flag if needed.
if (this._workColors.fg & FgFlags.INVERSE) {
if (bgOverride !== undefined && fgOverride === undefined) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
}
if (bgOverride === undefined && fgOverride !== undefined) {
// Resolve bg color type (default color has a different meaning in fg vs bg)
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
} else {
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
}
}
}
// Use the override if it exists
this._workColors.bg = bgOverride ?? this._workColors.bg;
this._workColors.fg = fgOverride ?? this._workColors.fg;
}
private _isCellSelected(x: number, y: number): boolean {
if (!this._model.selection.hasSelection) {
return false;
}
y -= this._terminal.buffer.active.viewportY;
if (this._model.selection.columnSelectMode) {
if (this._model.selection.startCol <= this._model.selection.endCol) {
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
}
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
}
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
const terminal = this._terminal;
// Selection does not exist
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
this._model.clearSelection();
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
this._model.clearSelection();
return;
}
this._model.selection.hasSelection = true;
this._model.selection.columnSelectMode = columnSelectMode;
this._model.selection.viewportStartRow = viewportStartRow;
this._model.selection.viewportEndRow = viewportEndRow;
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
this._model.selection.startCol = start[0];
this._model.selection.endCol = end[0];
this._rectangleRenderer!.updateBackgrounds(this._model);
}
/**
* Recalculates the character and canvas dimensions.
*/
private _updateDimensions(): void {
// TODO: Acquire CharSizeService properly
// Perform a new measure if the CharMeasure dimensions are not yet available
if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) {
if (!this._charSizeService.width || !this._charSizeService.height) {
return;
}
// Calculate the scaled character width. Width is floored as it must be
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
// blurry. When text is drawn to the grid not using the CharAtlas, it is
// clipped to ensure there is no overlap with the next cell.
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
// in order for the char atlas glyphs to not be blurry.
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
// NOTE: ceil fixes sometime, floor does others :s
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
// floating point number in order to ensure there is enough space to draw the character to the
// cell.
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
// will always be larger than device char height.
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
// Calculate the scaled character height. Height is ceiled in case
// devicePixelRatio is a floating point number in order to ensure there is
// enough space to draw the character to the cell.
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
// Calculate the scaled cell height, if lineHeight is not 1 then the value
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
// Calculate the device cell width, taking the letterSpacing into account.
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the x offset with a cell that text should draw from in order for it to be centered
// correctly within the cell.
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
// the canvas
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
// The the size of the canvas on the page. It's important that this rounds to nearest integer
// and not ceils as browsers often have floating point precision issues where
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
// large for the canvas element size.
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
// size on the canvas can differ.
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
}
// The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set
// window.devicePixelRatio as something like 1.100000023841858, when it's
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
// pixel too large for the canvas element size.
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
// Get the _actual_ dimensions of an individual cell. This needs to be
// derived from the canvasWidth/Height calculated above which takes into
// account window.devicePixelRatio. CharMeasure.width/height by itself is
// insufficient when the page is not at 100% zoom level as CharMeasure is
// measured in CSS pixels, but the actual char size on the canvas can
// differ.
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
// This fixes 110% and 125%, not 150% or 175% though
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
if (this._canvas.width === width && this._canvas.height === height) {
return;
}
// While the actual canvas size has changed, keep device canvas dimensions as the value before
// the change as it's an exact multiple of the cell sizes.
this._canvas.width = width;
this._canvas.height = height;
this._requestRedrawViewport();
}
private _requestRedrawViewport(): void {
@@ -621,3 +574,7 @@ export class JoinedCellData extends AttributeData implements ICellData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
function clamp(value: number, max: number, min: number = 0): number {
return Math.max(Math.min(value, max), min);
}
+9 -4
View File
@@ -3,6 +3,8 @@
* @license MIT
*/
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
/**
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
* top to bottom) to clip space.
@@ -50,9 +52,12 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar
return newArray;
}
export function throwIfFalsy<T>(value: T | undefined | null): T {
if (!value) {
throw new Error('value must not be falsy');
export class GLTexture {
public texture: WebGLTexture;
public version: number;
constructor(texture: WebGLTexture) {
this.texture = texture;
this.version = -1;
}
return value;
}
-36
View File
@@ -1,36 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { IColorSet } from 'browser/Types';
export interface IGlyphIdentifier {
chars: string;
code: number;
bg: number;
fg: number;
bold: boolean;
dim: boolean;
italic: boolean;
}
export interface ICharAtlasConfig {
customGlyphs: boolean;
devicePixelRatio: number;
letterSpacing: number;
lineHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet;
}
@@ -1,614 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'common/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { channels, rgba } from 'common/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
// For debugging purposes, it can be useful to set this to a really tiny value,
// to verify that LRU eviction works.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
/**
* The amount of the texture to be filled before throwing it away and starting
* again. Since the throw away and individual glyph draws don't cost too much,
* this prevent juggling multiple textures in the GL context.
*/
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
/**
* A shared object which is used to draw nothing for a particular cell.
*/
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
offset: { x: 0, y: 0 },
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
size: { x: 0, y: 0 },
sizeClipSpace: { x: 0, y: 0 }
};
const TMP_CANVAS_GLYPH_PADDING = 2;
export class WebglCharAtlas implements IDisposable {
private _didWarmUp: boolean = false;
private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
// The texture that the atlas is drawn to
public cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
private _tmpCanvas: HTMLCanvasElement;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// Since glyphs are expected to be around the same height, the packing
// strategy used it to fill a row with glyphs while keeping track of the
// tallest glyph in the row. Once the row is full a new row is started at
// (0,lastRow+lastRowTallestGlyph).
private _currentRowY: number = 0;
private _currentRowX: number = 0;
private _currentRowHeight: number = 0;
public hasCanvasChanged = false;
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
private _workAttributeData: AttributeData = new AttributeData();
constructor(
document: Document,
private _config: ICharAtlasConfig
) {
this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH;
this.cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
this._tmpCanvas = document.createElement('canvas');
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
}
public dispose(): void {
if (this.cacheCanvas.parentElement) {
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
}
}
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
private _doWarmUp(): void {
// Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
this._cacheMap[i] = {
[DEFAULT_COLOR]: {
[DEFAULT_COLOR]: rasterizedGlyph
}
};
}
}
public beginFrame(): boolean {
if (this._currentRowY > TEXTURE_CAPACITY) {
this.clearTexture();
this.warmUp();
return true;
}
return false;
}
public clearTexture(): void {
if (this._currentRowX === 0 && this._currentRowY === 0) {
return;
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._cacheMap = {};
this._cacheMapCombined = {};
this._currentRowHeight = 0;
this._currentRowX = 0;
this._currentRowY = 0;
this._didWarmUp = false;
}
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMapCombined[chars];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMapCombined[chars] = rasterizedGlyphSet;
}
let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(chars, bg, fg);
if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
/**
* Gets the glyphs texture coords, drawing the texture if it's not already
*/
public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMap[code];
if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {};
this._cacheMap[code] = rasterizedGlyphSet;
}
let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(code, bg, fg);
if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
}
return rasterizedGlyph;
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx >= this._config.colors.ansi.length) {
throw new Error('No color found for idx ' + idx);
}
return this._config.colors.ansi[idx];
}
private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor {
if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
}
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor);
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
// TODO: This object creation is slow
return {
rgba: bgColor << 8,
css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`
};
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.foreground;
}
return this._config.colors.background;
}
}
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
if (minimumContrastCss) {
return minimumContrastCss;
}
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).css;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
return channels.toCss(arr[0], arr[1], arr[2]);
case Attributes.CM_DEFAULT:
default:
if (inverse) {
const bg = this._config.colors.background.css;
if (bg.length === 9) {
// Remove bg alpha channel if present
return bg.slice(0, 7);
}
return bg;
}
return this._config.colors.foreground.css;
}
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor).rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.foreground.rgba;
}
return this._config.colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.background.rgba;
}
return this._config.colors.foreground.rgba;
}
}
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
if (!result) {
this._config.colors.contrastCache.setCss(bg, fg, null);
return undefined;
}
const css = channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
this._config.colors.contrastCache.setCss(bg, fg, css);
return css;
}
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
this.hasCanvasChanged = true;
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
// giant ligatures (eg. =====>) don't impact overall performance.
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.width < allowedWidth) {
this._tmpCanvas.width = allowedWidth;
}
// Include line height when drawing glyphs
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
if (this._tmpCanvas.height < allowedHeight) {
this._tmpCanvas.height = allowedHeight;
}
this._tmpCtx.save();
this._workAttributeData.fg = fg;
this._workAttributeData.bg = bg;
const invisible = !!this._workAttributeData.isInvisible();
if (invisible) {
return NULL_RASTERIZED_GLYPH;
}
const bold = !!this._workAttributeData.isBold();
const inverse = !!this._workAttributeData.isInverse();
const dim = !!this._workAttributeData.isDim();
const italic = !!this._workAttributeData.isItalic();
const underline = !!this._workAttributeData.isUnderline();
const strikethrough = !!this._workAttributeData.isStrikethrough();
let fgColor = this._workAttributeData.getFgColor();
let fgColorMode = this._workAttributeData.getFgColorMode();
let bgColor = this._workAttributeData.getBgColor();
let bgColorMode = this._workAttributeData.getBgColorMode();
if (inverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
// draw the background
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.globalCompositeOperation = 'source-over';
// draw the foreground/glyph
const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight;
const fontStyle = italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = TEXT_BASELINE;
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
// Apply alpha to dim the character
if (dim) {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
// Draw custom characters if applicable
let drawSuccess = false;
if (this._config.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
}
// Draw the character
if (!drawSuccess) {
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
}
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible,
// try for a maximum of 5 pixels.
if (chars === '_' && !this._config.allowTransparency) {
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
if (isBeyondCellBounds) {
for (let offset = 1; offset <= 5; offset++) {
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
if (!isBeyondCellBounds) {
break;
}
}
}
}
// Draw underline and strikethrough
if (underline || strikethrough) {
const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10));
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
this._tmpCtx.lineWidth = lineWidth;
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
this._tmpCtx.beginPath();
if (underline) {
this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset);
}
if (strikethrough) {
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
}
this._tmpCtx.stroke();
this._tmpCtx.closePath();
}
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
const imageData = this._tmpCtx.getImageData(
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
);
// TODO: Support transparency
// let isEmpty = false;
// if (!this._config.allowTransparency) {
// isEmpty = clearColor(imageData, backgroundColor);
// }
// Clear out the background color and determine if the glyph is empty.
const isEmpty = clearColor(imageData, backgroundColor);
// Handle empty glyphs
if (isEmpty) {
return NULL_RASTERIZED_GLYPH;
}
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
// Check if there is enough room in the current row and go to next if needed
if (this._currentRowX + rasterizedGlyph.size.x > TEXTURE_WIDTH) {
this._currentRowX = 0;
this._currentRowY += this._currentRowHeight;
this._currentRowHeight = 0;
}
// Record texture position
rasterizedGlyph.texturePosition.x = this._currentRowX;
rasterizedGlyph.texturePosition.y = this._currentRowY;
rasterizedGlyph.texturePositionClipSpace.x = this._currentRowX / TEXTURE_WIDTH;
rasterizedGlyph.texturePositionClipSpace.y = this._currentRowY / TEXTURE_HEIGHT;
// Update atlas current row
this._currentRowHeight = Math.max(this._currentRowHeight, rasterizedGlyph.size.y);
this._currentRowX += rasterizedGlyph.size.x;
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
return rasterizedGlyph;
}
/**
* Given an ImageData object, find the bounding box of the non-transparent
* portion of the texture and return an IRasterizedGlyph with these
* dimensions.
* @param imageData The image data to read.
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
*/
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
boundingBox.top = 0;
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
let found = false;
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.top = y;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.left = 0;
found = false;
for (let x = 0; x < width; x++) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.left = x;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.right = width;
found = false;
for (let x = width - 1; x >= 0; x--) {
for (let y = 0; y < height; y++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.right = x;
found = true;
break;
}
}
if (found) {
break;
}
}
boundingBox.bottom = height;
found = false;
for (let y = height - 1; y >= 0; y--) {
for (let x = 0; x < width; x++) {
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
if (imageData.data[alphaOffset] !== 0) {
boundingBox.bottom = y;
found = true;
break;
}
}
if (found) {
break;
}
}
return {
texturePosition: { x: 0, y: 0 },
texturePositionClipSpace: { x: 0, y: 0 },
size: {
x: boundingBox.right - boundingBox.left + 1,
y: boundingBox.bottom - boundingBox.top + 1
},
sizeClipSpace: {
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
},
offset: {
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0),
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
}
};
}
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
const width = boundingBox.right - boundingBox.left + 1;
const height = boundingBox.bottom - boundingBox.top + 1;
const clippedData = new Uint8ClampedArray(width * height * 4);
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
clippedData[newOffset] = imageData.data[oldOffset];
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
}
}
return new ImageData(clippedData, width, height);
}
}
/**
* Makes a partiicular rgb color in an ImageData completely transparent.
* @returns True if the result is "empty", meaning all pixels are fully transparent.
*/
function clearColor(imageData: ImageData, color: IColor): boolean {
let isEmpty = true;
const r = color.rgba >>> 24;
const g = color.rgba >>> 16 & 0xFF;
const b = color.rgba >>> 8 & 0xFF;
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
} else {
isEmpty = false;
}
}
return isEmpty;
}
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
@@ -4,46 +4,53 @@
*/
import { IRenderLayer } from './Types';
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { CellData } from 'common/buffer/CellData';
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
import { throwIfFalsy } from '../WebglUtils';
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { IOptionsService } from 'common/services/Services';
export abstract class BaseRenderLayer implements IRenderLayer {
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
private _canvas: HTMLCanvasElement;
protected _ctx!: CanvasRenderingContext2D;
private _scaledCharWidth: number = 0;
private _scaledCharHeight: number = 0;
private _scaledCellWidth: number = 0;
private _scaledCellHeight: number = 0;
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _deviceCharWidth: number = 0;
private _deviceCharHeight: number = 0;
private _deviceCellWidth: number = 0;
private _deviceCellHeight: number = 0;
private _deviceCharLeft: number = 0;
private _deviceCharTop: number = 0;
protected _charAtlas: WebglCharAtlas | undefined;
protected _charAtlas: ITextureAtlas | undefined;
constructor(
terminal: Terminal,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet
protected readonly _coreBrowserService: ICoreBrowserService,
protected readonly _optionsService: IOptionsService,
protected readonly _themeService: IThemeService
) {
super();
this._canvas = document.createElement('canvas');
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._initCanvas();
this._container.appendChild(this._canvas);
}
public dispose(): void {
this._container.removeChild(this._canvas);
if (this._charAtlas) {
this._charAtlas.dispose();
}
this.register(this._themeService.onChangeColors(e => {
this._refreshCharAtlas(terminal, e);
this.reset(terminal);
}));
this.register(toDisposable(() => {
this._canvas.remove();
this._charAtlas?.dispose();
}));
}
private _initCanvas(): void {
@@ -54,16 +61,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
}
public onOptionsChanged(terminal: Terminal): void {}
public onBlur(terminal: Terminal): void {}
public onFocus(terminal: Terminal): void {}
public onCursorMove(terminal: Terminal): void {}
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
public setColors(terminal: Terminal, colorSet: IColorSet): void {
this._refreshCharAtlas(terminal, colorSet);
}
public handleBlur(terminal: Terminal): void {}
public handleFocus(terminal: Terminal): void {}
public handleCursorMove(terminal: Terminal): void {}
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
@@ -80,8 +82,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(terminal, this._colors);
this.onGridChanged(terminal, 0, terminal.rows - 1);
this._refreshCharAtlas(terminal, this._themeService.colors);
this.handleGridChanged(terminal, 0, terminal.rows - 1);
}
/**
@@ -89,32 +91,32 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param terminal The terminal.
* @param colorSet The color set to use for the char atlas.
*/
private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
this._charAtlas.warmUp();
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
this._scaledCharHeight = dim.scaledCharHeight;
this._scaledCharLeft = dim.scaledCharLeft;
this._scaledCharTop = dim.scaledCharTop;
this._canvas.width = dim.scaledCanvasWidth;
this._canvas.height = dim.scaledCanvasHeight;
this._canvas.style.width = `${dim.canvasWidth}px`;
this._canvas.style.height = `${dim.canvasHeight}px`;
this._deviceCellWidth = dim.device.cell.width;
this._deviceCellHeight = dim.device.cell.height;
this._deviceCharWidth = dim.device.char.width;
this._deviceCharHeight = dim.device.char.height;
this._deviceCharLeft = dim.device.char.left;
this._deviceCharTop = dim.device.char.top;
this._canvas.width = dim.device.canvas.width;
this._canvas.height = dim.device.canvas.height;
this._canvas.style.width = `${dim.css.canvas.width}px`;
this._canvas.style.height = `${dim.css.canvas.height}px`;
// Draw the background if this is an opaque layer
if (!this._alpha) {
this._clearAll();
}
this._refreshCharAtlas(terminal, this._colors);
this._refreshCharAtlas(terminal, this._themeService.colors);
}
public abstract reset(terminal: Terminal): void;
@@ -128,10 +130,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCells(x: number, y: number, width: number, height: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
/**
@@ -142,10 +144,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
width * this._scaledCellWidth,
window.devicePixelRatio);
x * this._deviceCellWidth,
(y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
width * this._deviceCellWidth,
this._coreBrowserService.dpr);
}
/**
@@ -156,10 +158,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
window.devicePixelRatio * width,
this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
this._coreBrowserService.dpr * width,
this._deviceCellHeight);
}
/**
@@ -169,12 +171,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param y The row to fill.
*/
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
this._ctx.lineWidth = window.devicePixelRatio;
this._ctx.lineWidth = this._coreBrowserService.dpr;
this._ctx.strokeRect(
x * this._scaledCellWidth + window.devicePixelRatio / 2,
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
width * this._scaledCellWidth - window.devicePixelRatio,
(height * this._scaledCellHeight) - window.devicePixelRatio);
x * this._deviceCellWidth + this._coreBrowserService.dpr / 2,
y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2),
width * this._deviceCellWidth - this._coreBrowserService.dpr,
(height * this._deviceCellHeight) - this._coreBrowserService.dpr);
}
/**
@@ -184,7 +186,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -199,17 +201,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _clearCells(x: number, y: number, width: number, height: number): void {
if (this._alpha) {
this._ctx.clearRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
} else {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
width * this._scaledCellWidth,
height * this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
height * this._deviceCellHeight);
}
}
@@ -221,30 +223,30 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param cell The cell data for the character to draw.
* @param x The column to draw at.
* @param y The row to draw at.
* @param color The color of the character.
*/
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = TEXT_BASELINE;
this._clipRow(terminal, y);
this._clipCell(x, y, cell.getWidth());
this._ctx.fillText(
cell.getChars(),
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
x * this._deviceCellWidth + this._deviceCharLeft,
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
}
/**
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
* @param terminal The terminal.
* Clips a cell to ensure no pixels will be drawn outside of it.
* @param x The column to clip.
* @param y The row to clip.
* @param width The number of columns to clip.
*/
private _clipRow(terminal: Terminal, y: number): void {
private _clipCell(x: number, y: number, width: number): void {
this._ctx.beginPath();
this._ctx.rect(
0,
y * this._scaledCellHeight,
terminal.cols * this._scaledCellWidth,
this._scaledCellHeight);
x * this._deviceCellWidth,
y * this._deviceCellHeight,
width * this._deviceCellWidth,
this._deviceCellHeight);
this._ctx.clip();
}
@@ -257,7 +259,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * this._coreBrowserService.dpr}px ${terminal.options.fontFamily}`;
}
}
@@ -7,9 +7,12 @@ import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { IColorSet, ITerminal } from 'browser/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ICoreService, IOptionsService } from 'common/services/Services';
import { toDisposable } from 'common/Lifecycle';
import { isFirefox } from 'common/Platform';
interface ICursorState {
x: number;
@@ -34,11 +37,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
private readonly _terminal: ITerminal,
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>,
coreBrowserService: ICoreBrowserService,
private readonly _coreService: ICoreService,
optionsService: IOptionsService,
themeService: IThemeService
) {
super(container, 'cursor', zIndex, true, colors);
super(terminal, container, 'cursor', zIndex, true, coreBrowserService, optionsService, themeService);
this._state = {
x: 0,
y: 0,
@@ -51,7 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
this.onOptionsChanged(terminal);
this._handleOptionsChanged(terminal);
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal)));
this.register(toDisposable(() => {
this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager = undefined;
}));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
@@ -69,25 +79,25 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(terminal: Terminal): void {
this._clearCursor();
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
this.onOptionsChanged(terminal);
this._handleOptionsChanged(terminal);
}
public onBlur(terminal: Terminal): void {
public handleBlur(terminal: Terminal): void {
this._cursorBlinkStateManager?.pause();
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public onFocus(terminal: Terminal): void {
public handleFocus(terminal: Terminal): void {
this._cursorBlinkStateManager?.resume(terminal);
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public onOptionsChanged(terminal: Terminal): void {
private _handleOptionsChanged(terminal: Terminal): void {
if (terminal.options.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
this._cursorBlinkStateManager = new CursorBlinkStateManager(() => {
this._render(terminal, true);
});
}, this._coreBrowserService);
}
} else {
this._cursorBlinkStateManager?.dispose();
@@ -98,11 +108,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public onCursorMove(terminal: Terminal): void {
public handleCursorMove(terminal: Terminal): void {
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
}
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
@@ -112,8 +122,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden
// TODO: Need to expose API for this
if (!this._terminal.coreService.isCursorInitialized || this._terminal.coreService.isCursorHidden) {
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor();
return;
}
@@ -136,16 +145,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
return;
}
if (!isTerminalFocused(terminal)) {
if (!this._coreBrowserService.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
const cursorStyle = terminal.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
}
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
@@ -165,7 +170,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
// The cursor is already in the correct spot, don't redraw
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === isTerminalFocused(terminal) &&
this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === terminal.options.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
@@ -186,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _clearCursor(): void {
if (this._state) {
// Avoid potential rounding errors when device pixel ratio is less than 1
if (window.devicePixelRatio < 1) {
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
// less than 1
if (isFirefox || this._coreBrowserService.dpr < 1) {
this._clearAll();
} else {
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
@@ -204,30 +210,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!);
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillCells(x, y, cell.getWidth(), 1);
this._ctx.fillStyle = this._colors.cursorAccent.css;
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
this._fillCharTrueColor(terminal, cell, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._ctx.fillStyle = this._themeService.colors.cursor.css;
this._fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor.css;
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
this._ctx.restore();
}
@@ -248,11 +254,11 @@ class CursorBlinkStateManager {
private _animationTimeRestarted: number | undefined;
constructor(
terminal: Terminal,
private _renderCallback: () => void
private _renderCallback: () => void,
private _coreBrowserService: ICoreBrowserService
) {
this.isCursorVisible = true;
if (isTerminalFocused(terminal)) {
if (this._coreBrowserService.isFocused) {
this._restartInterval();
}
}
@@ -261,15 +267,15 @@ class CursorBlinkStateManager {
public dispose(): void {
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -283,7 +289,7 @@ class CursorBlinkStateManager {
// Force a cursor render to ensure it's visible and in the correct position
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
@@ -293,7 +299,7 @@ class CursorBlinkStateManager {
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
@@ -301,7 +307,7 @@ class CursorBlinkStateManager {
// the regular interval is setup in order to support restarting the blink
// animation in a lightweight way (without thrashing clearInterval and
// setInterval).
this._blinkStartTimeout = window.setTimeout(() => {
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
// Check if another animation restart was requested while this was being
// started
if (this._animationTimeRestarted) {
@@ -315,13 +321,13 @@ class CursorBlinkStateManager {
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
// Setup the blink interval
this._blinkInterval = window.setInterval(() => {
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
// Adjust the animation time if it was restarted
if (this._animationTimeRestarted) {
// calc time diff
@@ -334,7 +340,7 @@ class CursorBlinkStateManager {
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._renderCallback();
this._animationFrame = undefined;
});
@@ -345,15 +351,15 @@ class CursorBlinkStateManager {
public pause(): void {
this.isCursorVisible = true;
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._coreBrowserService.window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout);
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = undefined;
}
if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame);
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = undefined;
}
}
@@ -367,7 +373,3 @@ class CursorBlinkStateManager {
this.restartBlinkAnimation(terminal);
}
}
function isTerminalFocused(terminal: Terminal): boolean {
return document.activeElement === terminal.textarea && document.hasFocus();
}
@@ -3,23 +3,31 @@
* @license MIT
*/
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { Terminal } from 'xterm';
import { BaseRenderLayer } from './BaseRenderLayer';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { is256Color } from '../atlas/CharAtlasUtils';
import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkifierEvent | undefined;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) {
super(container, 'link', zIndex, true, colors);
terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
constructor(
container: HTMLElement,
zIndex: number,
terminal: Terminal,
linkifier2: ILinkifier2,
coreBrowserService: ICoreBrowserService,
optionsService: IOptionsService,
themeService: IThemeService
) {
super(terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService);
terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
@@ -44,14 +52,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
private _onShowLinkUnderline(e: ILinkifierEvent): void {
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
if (e.fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillStyle = this._themeService.colors.background.css;
} else if (e.fg !== undefined && is256Color(e.fg)) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
this._ctx.fillStyle = this._themeService.colors.foreground.css;
}
if (e.y1 === e.y2) {
@@ -68,7 +76,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
this._state = e;
}
private _onHideLinkUnderline(e: ILinkifierEvent): void {
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
this._clearCurrentLink();
}
}
@@ -4,45 +4,34 @@
*/
import { IDisposable, Terminal } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { IRenderDimensions } from 'browser/renderer/shared/Types';
export interface IRenderLayer extends IDisposable {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: Terminal): void;
handleBlur(terminal: Terminal): void;
/**
* * Called when the terminal gets focus.
* Called when the terminal gets focus.
*/
onFocus(terminal: Terminal): void;
handleFocus(terminal: Terminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: Terminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: Terminal): void;
/**
* Called when the theme changes.
*/
setColors(terminal: Terminal, colorSet: IColorSet): void;
handleCursorMove(terminal: Terminal): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
/**
* Registers a handler to join characters to render as a group
+2 -2
View File
@@ -1,10 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"dom",
"es6",
"es6"
],
"rootDir": ".",
"outDir": "../out",
@@ -252,7 +252,7 @@ describe('WebGL Renderer Integration Tests', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
data += `\\x1b[38;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -312,7 +312,7 @@ describe('WebGL Renderer Integration Tests', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`;
data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -348,7 +348,56 @@ describe('WebGL Renderer Integration Tests', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`;
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(page, data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.slice(1, 3), 16);
const g = parseInt(cssColor.slice(3, 5), 16);
const b = parseInt(cssColor.slice(5, 7), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
itWebgl('foreground 16-255 dim', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[2;38;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(page, data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.slice(1, 3), 16);
const g = parseInt(cssColor.slice(3, 5), 16);
const b = parseInt(cssColor.slice(5, 7), 16);
// It's difficult to assert the exact color due to rounding, just ensure the color differs
// to the regular color
await pollFor(page, async () => {
const c = await getCellColor(x + 1, y + 1);
return (
(c[0] === 0 || c[0] !== r) &&
(c[1] === 0 || c[1] !== g) &&
(c[2] === 0 || c[2] !== b)
);
}, true);
}
}
});
itWebgl('background 16-255 dim', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`;
}
data += '\\r\\n';
}
@@ -369,7 +418,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
data += `\\x1b[38;2;${i};0;0m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -405,7 +454,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
data += `\\x1b[38;2;0;${i};0m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -441,7 +490,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
data += `\\x1b[38;2;0;0;${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -477,7 +526,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
data += `\\x1b[38;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -567,7 +616,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`;
data += `\\x1b[7;48;2;0;${i};0m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -603,7 +652,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`;
data += `\\x1b[7;48;2;0;0;${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -639,7 +688,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`;
data += `\\x1b[7;48;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -674,7 +723,7 @@ describe('WebGL Renderer Integration Tests', async () => {
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`;
data += `\\x1b[8;48;2;${i};${i};${i}m█\\x1b[0m`;
}
data += '\\r\\n';
}
@@ -832,7 +881,7 @@ describe('WebGL Renderer Integration Tests', async () => {
});
});
describe('selection', async () => {
describe('selectionBackground', async () => {
if (areTestsEnabled) {
before(async () => setupBrowser());
after(async () => browser.close());
@@ -843,7 +892,7 @@ describe('WebGL Renderer Integration Tests', async () => {
const theme: ITheme = {
foreground: '#FF0000',
background: '#00FF00',
selection: '#0000FF'
selectionBackground: '#0000FF'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
await writeSync(page, ` █\\x1b[7m█\\x1b[0m`);
@@ -860,7 +909,7 @@ describe('WebGL Renderer Integration Tests', async () => {
describe('allowTransparency', async () => {
if (areTestsEnabled) {
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true }));
before(async () => setupBrowser({ allowTransparency: true }));
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
}
@@ -870,7 +919,7 @@ describe('WebGL Renderer Integration Tests', async () => {
background: '#ff000080'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\\x1b[7m█\x1b[0m`;
const data = `\\x1b[7m█\\x1b[0m`;
await writeSync(page, data);
// Inverse background should be opaque
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
@@ -879,7 +928,7 @@ describe('WebGL Renderer Integration Tests', async () => {
describe('selectionForeground', () => {
if (areTestsEnabled) {
before(async () => setupBrowser({ rendererType: 'dom' }));
before(async () => setupBrowser());
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
}
@@ -889,7 +938,7 @@ describe('WebGL Renderer Integration Tests', async () => {
selectionForeground: '#ff0000'
};
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
const data = `\\x1b[7m█\x1b[0m`;
const data = `\\x1b[7m█\\x1b[0m`;
await writeSync(page, data);
await page.evaluate(`window.term.selectAll()`);
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
@@ -898,7 +947,7 @@ describe('WebGL Renderer Integration Tests', async () => {
describe('decoration color overrides', async () => {
if (areTestsEnabled) {
before(async () => setupBrowser({ rendererType: 'dom' }));
before(async () => setupBrowser());
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
}
@@ -992,8 +1041,8 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
window.result = new Uint8Array(4);
window.d = window.term._core._renderService.dimensions;
window.gl.readPixels(
Math.floor((${col - 0.5}) * window.d.scaledCellWidth),
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight),
Math.floor((${col - 0.5}) * window.d.device.cell.width),
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height),
1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
);
`);
@@ -1003,18 +1052,18 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
async function getCellPixels(col: number, row: number): Promise<number[]> {
await page.evaluate(`
window.gl = window.term._core._renderService._renderer._gl;
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4);
window.d = window.term._core._renderService.dimensions;
window.gl.readPixels(
Math.floor(${col - 1} * window.d.scaledCellWidth),
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
Math.floor(${col - 1} * window.d.device.cell.width),
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height),
window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
);
`);
return await page.evaluate(`Array.from(window.result)`);
}
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
async function setupBrowser(options: ITerminalOptions = {}): Promise<void> {
browser = await launchBrowser();
page = await (await browser.newContext()).newPage();
await page.setViewportSize({ width, height });
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"target": "es2015",
"lib": [
"es6",
],
+15 -5
View File
@@ -12,6 +12,21 @@ declare module 'xterm-addon-webgl' {
export class WebglAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
/**
* An event that is fired when the renderer loses its canvas context.
*/
public readonly onContextLoss: IEvent<void>;
/**
* An event that is fired when the texture atlas of the renderer changes.
*/
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
/**
* An event that is fired when the a new page is added to the texture atlas.
*/
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
constructor(preserveDrawingBuffer?: boolean);
/**
@@ -29,10 +44,5 @@ declare module 'xterm-addon-webgl' {
* Clears the terminal's texture atlas and triggers a redraw.
*/
public clearTextureAtlas(): void;
/**
* Fired when the WebglRenderer loses context
*/
public get onContextLoss(): IEvent<void>;
}
}
+19 -16
View File
@@ -1,16 +1,19 @@
# Node.js
# Build a general Node.js application with npm.
# Add steps that analyze code, save build artifacts, deploy, and more:
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
pr:
branches:
include: ["main"]
trigger:
branches:
include: ["main"]
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
@@ -39,11 +42,11 @@ jobs:
- job: macOS
pool:
vmImage: 'macOS-10.15'
vmImage: 'macOS-11'
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: CacheBeta@1
inputs:
@@ -63,7 +66,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: CacheBeta@1
inputs:
@@ -79,7 +82,7 @@ jobs:
- job: Linux_IntegrationTests
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- script: |
# source: https://github.com/microsoft/playwright/issues/1041
@@ -92,7 +95,7 @@ jobs:
displayName: Install required packages
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
@@ -108,11 +111,11 @@ jobs:
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
# - job: macOS_IntegrationTests
# pool:
# vmImage: 'macOS-10.15'
# vmImage: 'macOS-11'
# steps:
# - task: NodeTool@0
# inputs:
# versionSpec: '14.x'
# versionSpec: '18.x'
# displayName: 'Install Node.js'
# - script: yarn --frozen-lockfile
# displayName: 'Install dependencies and build'
@@ -129,7 +132,7 @@ jobs:
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
@@ -148,11 +151,11 @@ jobs:
- Windows_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
versionSpec: '18.x'
displayName: 'Install Node.js'
- task: YarnInstaller@3
inputs:
+8 -5
View File
@@ -82,14 +82,15 @@ This document lists xterm.js' support of terminal sequences. The sequences are g
- OSC - Operating System Command: sequence starting with \`ESC ]\` (7bit) or OSC (\`\\x9D\`, 8bit)
Application Program Command (APC), Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
any sequence of these types will be ignored. They are also not hookable by the API.
any sequence of these types will be silently ignored. They are also not hookable by the API.
Note that the list only contains sequences implemented in xterm.js' core codebase. Missing sequences are either
not supported or unstable/experimental. Furthermore addons or integrations can provide additional custom sequences.
Note that the list only marks sequences implemented in xterm.js' core codebase as supported. Missing sequences are either
not supported or unstable/experimental. Furthermore addons or integrations can provide additional custom sequences
(denoted as "External" where known).
To denote the sequences the tables use the same abbreviations as xterm does:
- \`Ps\`: A single (usually optional) numeric parameter, composed of one or more decimal digits.
- \`Pm\`: A multiple numeric parameter composed of any number of single numeric parameters, separated by ; character(s),
- \`Pm\`: Multiple numeric parameters composed of any number of single numeric parameters, separated by ; character(s),
e.g. \` Ps ; Ps ; ... \`.
- \`Pt\`: A text parameter composed of printable characters. Note that for most commands with \`Pt\` only
ASCII printables are specified to work. Additionally the parser will let pass any codepoint greater than C1 as printable.
@@ -334,7 +335,9 @@ const MACRO = [
// #P[reason] - partial support with a reason as title
[/#P\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Partial</span>`],
// #B[reason] - supported but broken in a certain way, reason in title
[/#B\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Broken</span>`]
[/#B\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Broken</span>`],
// #E[notes] - support via external resource, eg. addon
[/#E\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">External</span>`]
];
function applyMacros(s) {
+1
View File
@@ -28,6 +28,7 @@ if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
// Publish addons if any files were changed inside of the addon
const addonPackageDirs = [
path.resolve(__dirname, '../addons/xterm-addon-attach'),
path.resolve(__dirname, '../addons/xterm-addon-canvas'),
path.resolve(__dirname, '../addons/xterm-addon-fit'),
path.resolve(__dirname, '../addons/xterm-addon-ligatures'),
path.resolve(__dirname, '../addons/xterm-addon-search'),
+22 -5
View File
@@ -4,7 +4,7 @@
*
* Script to test different mouse modes in terminal emulators.
* Tests for protocols DECSET 9, 1000, 1002, 1003 with different
* report encodings (default, UTF8, SGR, URXVT).
* report encodings (default, UTF8, SGR, URXVT, SGR-pixels).
*
* VT200 Highlight mode (DECSET 1001) is not implemented.
*
@@ -152,13 +152,27 @@ const ENC = {
],
'URXVT': [
'\x1b[?1015h',
// format: CSI <button + 32> ; Prow ; Pcol M
// format: CSI <button + 32> ; Prow ; Pcol M
report => {
// strip off introducer + M
const sReport = report.toString().slice(2, -1);
const [button, col, row] = sReport.split(';');
return {state: evalButtonCode(button - 32), row, col};
}
],
'SGR_PIXELS' : [
'\x1b[?1016h',
// format: CSI < Pbutton ; Prow ; Pcol M
report => {
// strip off introducer + M
const sReport = report.toString().slice(3, -1);
const [buttonCode, col, row] = sReport.split(';');
const state = evalButtonCode(buttonCode);
if (report[report.length - 1] === 'm'.charCodeAt(0)) {
state.action = 'release';
}
return {state, row, col};
}
]
}
@@ -203,9 +217,12 @@ function activate() {
function applyReportData(data) {
let {state, row, col} = ENC[Object.keys(ENC)[activeEnc]][1](data);
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
// apply to cursor position
process.stdout.write(`\x1b[${row};${col}H`);
if (Object.keys(ENC)[activeEnc] !== 'SGR_PIXELS') {
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
process.stdout.write(`\x1b[${row};${col}H`);
} else {
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'x:', row, 'y:', col);
}
}
printMenu();
+23 -4
View File
@@ -149,6 +149,7 @@
right: 0;
z-index: 10;
color: transparent;
pointer-events: none;
}
.xterm .live-region {
@@ -160,13 +161,27 @@
}
.xterm-dim {
opacity: 0.5;
/* Dim should not apply to background, so the opacity of the foreground color is applied
* explicitly in the generated class and reset to 1 here */
opacity: 1 !important;
}
.xterm-underline {
text-decoration: underline;
.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }
.xterm-overline {
text-decoration: overline;
}
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
.xterm-strikethrough {
text-decoration: line-through;
}
@@ -176,8 +191,12 @@
position: absolute;
}
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
z-index: 7;
}
.xterm-decoration-overview-ruler {
z-index: 7;
z-index: 8;
position: absolute;
top: 0;
right: 0;
+655 -116
View File
File diff suppressed because it is too large Load Diff
+18 -2
View File
@@ -10,8 +10,6 @@
<link rel="shortcut icon" type="image/png" href="/logo.png">
<link rel="stylesheet" href="/xterm.css" />
<link rel="stylesheet" href="/style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
</head>
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
@@ -25,6 +23,7 @@
<button id= "addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
<button id= "stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
<button id= "testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
<button id= "vtbutton" class="tabLinks" onclick="openSection(event, 'vt')">VT</button>
</div>
<div id="options" class="tabContent">
<h3>Options</h3>
@@ -70,22 +69,39 @@
<dl>
<dt>Lifecycle</dt>
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
<dd><button id="create-new-window" title="This is used to test rendering in other windows">Create terminal in new window</button></dd>
<dt>Performance</dt>
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
<dt>Styles</dt>
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
<dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
</dl>
</div>
</div>
<div id="vt" class="tabContent">
<h3>VT</h3>
<div id="vt-container"></div>
</div>
</div>
</div>
<input type="checkbox" id="texture-atlas-zoom"/>
<label for="texture-atlas-zoom">Zoom texture atlas</label>
<div id="texture-atlas"></div>
<script src="dist/client-bundle.js" defer ></script>
<script>
var tab = localStorage.getItem("tab");
+47 -23
View File
@@ -17,22 +17,23 @@ function startServer() {
expressWs(app);
var terminals = {},
logs = {};
unsentOutput = {},
temporaryDisposable = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/logo.png', (req, res) => {
res.sendFile(__dirname + '/logo.png');
});
app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/test', (req, res) => {
res.sendFile(__dirname + '/test.html');
});
app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
app.get('/style.css', (req, res) => {
res.sendFile(__dirname + '/style.css');
});
@@ -44,7 +45,7 @@ function startServer() {
env['COLORTERM'] = 'truecolor';
var cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows),
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
term = pty.spawn(process.platform === 'win32' ? 'pwsh.exe' : 'bash', [], {
name: 'xterm-256color',
cols: cols || 80,
rows: rows || 24,
@@ -55,9 +56,9 @@ function startServer() {
console.log('Created terminal with PID: ' + term.pid);
terminals[term.pid] = term;
logs[term.pid] = '';
term.on('data', function(data) {
logs[term.pid] += data;
unsentOutput[term.pid] = '';
temporaryDisposable[term.pid] = term.onData(function(data) {
unsentOutput[term.pid] += data;
});
res.send(term.pid.toString());
res.end();
@@ -77,15 +78,29 @@ function startServer() {
app.ws('/terminals/:pid', function (ws, req) {
var term = terminals[parseInt(req.params.pid)];
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
temporaryDisposable[term.pid].dispose();
delete temporaryDisposable[term.pid];
ws.send(unsentOutput[term.pid]);
delete unsentOutput[term.pid];
// unbuffered delivery after user input
let userInput = false;
// string message buffering
function buffer(socket, timeout) {
function buffer(socket, timeout, maxSize) {
let s = '';
let sender = null;
return (data) => {
s += data;
if (!sender) {
if (s.length > maxSize || userInput) {
userInput = false;
socket.send(s);
s = '';
if (sender) {
clearTimeout(sender);
sender = null;
}
} else if (!sender) {
sender = setTimeout(() => {
socket.send(s);
s = '';
@@ -95,29 +110,38 @@ function startServer() {
};
}
// binary message buffering
function bufferUtf8(socket, timeout) {
let buffer = [];
let sender = null;
function bufferUtf8(socket, timeout, maxSize) {
const chunks = [];
let length = 0;
let sender = null;
return (data) => {
buffer.push(data);
chunks.push(data);
length += data.length;
if (!sender) {
sender = setTimeout(() => {
socket.send(Buffer.concat(buffer, length));
buffer = [];
if (length > maxSize || userInput) {
userInput = false;
socket.send(Buffer.concat(chunks));
chunks.length = 0;
length = 0;
if (sender) {
clearTimeout(sender);
sender = null;
}
} else if (!sender) {
sender = setTimeout(() => {
socket.send(Buffer.concat(chunks));
chunks.length = 0;
length = 0;
sender = null;
}, timeout);
}
};
}
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144);
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
// it could flood the communication channel and make the terminal unresponsive. Learn more about
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
term.on('data', function(data) {
term.onData(function(data) {
try {
send(data);
} catch (ex) {
@@ -126,13 +150,13 @@ function startServer() {
});
ws.on('message', function(msg) {
term.write(msg);
userInput = true;
});
ws.on('close', function () {
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
delete logs[term.pid];
});
});
+17 -1
View File
@@ -45,7 +45,6 @@ pre {
#container {
display: flex;
height: 75vh;
}
#grid {
flex: 1;
@@ -90,3 +89,20 @@ pre {
max-height: 100vh;
overflow-y: auto;
}
#texture-atlas-zoom:checked + label + #texture-atlas canvas {
/* Zoom atlas to the width of the container*/
width: 100% !important;
height: auto !important;
}
#texture-atlas {
width: 100%;
}
#texture-atlas canvas {
image-rendering: pixelated;
border: 1px solid #ccc;
}
.vt-button * {
margin-right: 1em;
}
+6 -5
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.19.0",
"version": "5.2.1",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -58,7 +58,7 @@
"@types/glob": "^7.2.0",
"@types/jsdom": "^16.2.13",
"@types/mocha": "^9.0.0",
"@types/node": "^14.14.44",
"@types/node": "^18.16.0",
"@types/utf8": "^3.0.0",
"@types/webpack": "^5.28.0",
"@types/ws": "^8.2.0",
@@ -68,19 +68,20 @@
"cross-env": "^7.0.3",
"deep-equal": "^2.0.5",
"eslint": "^8.1.0",
"eslint-plugin-jsdoc": "^39.3.6",
"express": "^4.17.1",
"express-ws": "^5.0.2",
"glob": "^7.2.0",
"jsdom": "^18.0.1",
"mocha": "^9.2.0",
"mocha": "^10.1.0",
"mustache": "^4.2.0",
"node-pty": "^0.10.1",
"nyc": "^15.1.0",
"playwright": "^1.22.1",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
"ts-loader": "^9.1.2",
"typescript": "^4.4.4",
"ts-loader": "^9.3.1",
"typescript": "4.7",
"utf8": "^3.0.0",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",

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