Compare commits

..
178 Commits
Author SHA1 Message Date
Daniel Imms 10e4d075a3 Merge pull request #3589 from Tyriar/v4_16
v4.16.0
2021-12-22 10:32:27 -08:00
Daniel Imms 35c2d1ac4a v4.16.0 2021-12-22 10:26:30 -08:00
Daniel Imms 86ad5198ac Merge pull request #3588 from Tyriar/3505
Log characters as a code array when parsing data as well
2021-12-22 10:06:55 -08:00
Daniel Imms 9a3ee49160 Log characters as a code array when parsing data as well
Makes it consistent with sending data debug logs

Fixes #3505
2021-12-22 09:58:15 -08:00
Daniel Imms 3da594abb0 Merge pull request #3587 from Tyriar/3493
Make link provider API stable
2021-12-22 09:50:44 -08:00
Daniel Imms 72308b4f7f Make link provider API stable
Fixes #3493
2021-12-22 09:50:11 -08:00
Daniel Imms c22b3a92d7 Merge pull request #3236 from gera2ld/master
Search Addon: Fix length calculation of wide unicode chars
2021-12-22 09:31:56 -08:00
Daniel Imms f685360707 Add docs to line cache entry items 2021-12-22 09:29:32 -08:00
Daniel Imms 0fcd87085b Name line cache entry type 2021-12-22 09:26:19 -08:00
Daniel Imms 8d2e4328ca Merge branch 'master' into master 2021-12-22 09:12:00 -08:00
Daniel Imms ae6f5754f2 Merge pull request #3524 from jerch/osc_colors
OSC 4/10/11/12 color handling
2021-12-22 09:10:37 -08:00
Daniel Imms 086ca58ece Comment tweaks 2021-12-22 09:10:12 -08:00
Daniel Imms e635b3d507 Merge remote-tracking branch 'upstream/master' into pr/jerch/3524 2021-12-22 08:56:36 -08:00
Daniel Imms df548e1106 Merge pull request #3586 from Tyriar/3585
Fix demo
2021-12-22 08:55:58 -08:00
Daniel Imms 13b8c78947 Fix demo
Fixes #3585
2021-12-22 08:55:39 -08:00
Daniel Imms 3cc9994a7d Merge remote-tracking branch 'upstream/master' into pr/jerch/3524 2021-12-22 08:28:43 -08:00
Daniel Imms d95d4b7dd4 Merge pull request #3583 from Tyriar/3552
Don't include trailing EOL when selecting multiple lines
2021-12-21 10:59:57 -08:00
Daniel Imms d261c3b494 Merge branch 'master' into 3552 2021-12-21 10:50:28 -08:00
Daniel Imms 8c94c97a28 Don't include trailing EOL when selecting multiple lines
Fixes #3552
2021-12-21 10:49:58 -08:00
Daniel Imms 46c316b795 Merge pull request #3546 from silamon/options2
Allow setting multiple options through term.options
2021-12-21 09:41:04 -08:00
Daniel Imms 7535d2e527 Move options test to Terminal.api.ts 2021-12-21 09:13:53 -08:00
Daniel Imms 6cc1708abe Merge branch 'master' into options2 2021-12-21 08:40:46 -08:00
Daniel Imms e623672e81 Merge pull request #3536 from silamon/isWrappedEraseInBufferline
Clear isWrapped in eraseBufferLine
2021-12-21 08:39:03 -08:00
Daniel Imms 006e0b7513 Merge branch 'master' into isWrappedEraseInBufferline 2021-12-21 08:30:46 -08:00
Daniel Imms c5b5f3610e Merge pull request #3582 from Tyriar/deprecated_event
Mark IKeyboardEvent.keyCode as deprecated
2021-12-20 10:03:40 -08:00
Daniel Imms 6571298061 Mark IKeyboardEvent.keyCode as deprecated 2021-12-20 10:02:49 -08:00
Daniel Imms 9350a8b237 Merge pull request #3580 from Tyriar/3579
Have linkifier2 use screen element for link detection
2021-12-20 08:57:16 -08:00
Daniel Imms f4c263f380 Merge branch 'master' into 3579 2021-12-20 08:46:50 -08:00
Daniel Imms 9a920a10f3 Have linkifier2 use screen element for link detection
Fixes #3579
2021-12-20 08:46:13 -08:00
Daniel Imms 2381096b06 Merge pull request #3577 from SquitchYT/master
Update README.md
2021-12-20 07:14:02 -08:00
Squitch 0ddb43ac8a Update README.md 2021-12-18 16:40:58 +01:00
Daniel Imms 45705e0614 Merge branch 'master' into options2 2021-12-17 11:13:31 -08:00
Daniel Imms 6d6e8845c1 Merge pull request #3553 from jerch/fix_3548
fix canvas renderer color changes.
2021-12-17 11:10:43 -08:00
Daniel Imms 5facfe55b8 Merge pull request #3535 from Eugeny/fix-duplicate-ime-input
input: prevent duplicate IME input on Linux - fixes #3533
2021-12-17 08:49:49 -08:00
Daniel Imms 35cd505e9e Merge pull request #3554 from acid-chicken/microsoft-vscode-137047
wide characters overflow the cache canvas on WebGL render
2021-12-16 06:25:13 -08:00
Daniel Imms 78d3e22052 Merge pull request #3576 from Heysunk/master
Added support for custom regex to WebLinkProvider
2021-12-15 12:18:50 -08:00
Daniel Imms 7157b03a7e Change interface to use ?
So it's not a breaking change
2021-12-15 12:04:07 -08:00
Daniel Imms 7663e4a4e3 Simplify with || 2021-12-15 12:03:29 -08:00
Michael Chlebek ddc3d07bd4 Added support for custom regex to WebLinkProvider 2021-12-15 18:21:31 +01:00
Megan Rogge f6d3fae25e Merge branch 'master' into microsoft-vscode-137047 2021-12-14 14:44:55 -06:00
Daniel Imms 952d0efde2 Merge pull request #3574 from Tyriar/clean_dead_key
Clear unprocessed dead key state on input or press
2021-12-10 10:15:24 -08:00
Daniel Imms b7c3397471 Clear unprocessed dead key state on input or press
Fixes #3573
2021-12-09 13:14:43 -08:00
Daniel Imms cde4277e84 Merge pull request #3572 from Tyriar/a11y_tab_index
Add tabIndex to xterm-accessibility
2021-12-09 12:41:28 -08:00
Daniel Imms 37b54f8e8a Merge branch 'master' into a11y_tab_index 2021-12-09 12:31:30 -08:00
Daniel Imms 1304fd8a72 Add tabIndex to xterm-accessibility
See microsoft/vscode#135920
2021-12-09 20:30:38 +00:00
Daniel Imms 7b072893be Merge pull request #3565 from LabhanshAgrawal/characterJoiner
trycatch characterJoiner handlers
2021-11-24 10:55:54 -08:00
Labhansh Agrawal d10d5be222 make requested changes 2021-11-25 00:11:44 +05:30
Labhansh Agrawal 070afc64c8 trycatch characterJoiner handlers 2021-11-24 18:27:33 +05:30
Daniel Imms 20141518d9 Merge pull request #3560 from silamon/headless-compile-error
Remove headless include in benchmark projects
2021-11-21 03:50:33 -08:00
Simon Lamon d85a57118e remove headless include in benchmark projects 2021-11-20 17:48:45 +00:00
Simon Lamon d7d85870d2 remove headless include in benchmark projects 2021-11-20 17:33:38 +00:00
Acid Chicken (硫酸鶏) dbd3766a9d chore: reflect reviews 2021-11-13 01:36:46 +09:00
Acid Chicken (硫酸鶏) 935a1ba903 fix(xterm-addon-webgl): wide characters overflow the cache canvas 2021-11-13 00:32:24 +09:00
Jörg Breitbart 0c20589a7f remove temp atlas fix 2021-11-10 23:00:36 +01:00
Jörg Breitbart 84325627aa cleanup 2021-11-10 22:36:50 +01:00
Jörg Breitbart 9964210ac4 implement OSC 104|110|111|112 2021-11-10 22:15:35 +01:00
Jörg Breitbart e62e82ba10 Merge branch 'master' into osc_colors 2021-11-10 19:17:54 +01:00
Jörg Breitbart 5a468c0877 fix #3548 2021-11-10 18:02:00 +01:00
Simon Lamon 71257ec00e Fix tests 2021-11-10 10:45:42 +00:00
Simon Lamon 4245ce33b5 Remove publicOptions completely 2021-11-10 10:17:14 +00:00
Simon Lamon 317641c3bb Add some tests 2021-11-10 09:42:45 +00:00
Simon Lamon 2c65d96863 Fix tests 2021-11-10 08:39:28 +00:00
silamonandDaniel Imms b1c179461d Update typings/xterm.d.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2021-11-10 09:15:43 +01:00
Daniel Imms 0bc02d6838 Merge pull request #3551 from Tyriar/scroll
Allow shift+wheel to bubble up the DOM
2021-11-09 06:54:22 -08:00
Daniel Imms d54c40a3ed Allow shift+wheel to bubble up the DOM
Fixes #3550
2021-11-09 06:46:00 -08:00
Jörg Breitbart 896f5a7e4c extend logic to OSC 12 cursor color 2021-11-09 13:46:54 +01:00
Jörg Breitbart 4be063be0f Merge branch 'master' into osc_colors 2021-11-08 20:57:54 +01:00
Jörg Breitbart 855ed636e7 remove only from tests 2021-11-08 20:57:22 +01:00
Jörg Breitbart b8097f53a6 OSC 4 integration tests 2021-11-08 20:55:47 +01:00
Jörg Breitbart 54822af73a use xcolor parser for OSC 4 2021-11-08 19:40:35 +01:00
Jörg Breitbart b55ee772b4 fix inputhandler type definition 2021-11-08 19:01:30 +01:00
Jörg Breitbart 56f0e69e1f api tests for OSC 10 & 11 2021-11-08 18:36:11 +01:00
Jörg Breitbart 064f683781 make linter happy 2021-11-08 17:23:34 +01:00
Jörg Breitbart 54375713bc simplify color events 2021-11-08 17:17:39 +01:00
Daniel Imms cb3dd19039 Merge pull request #3538 from coderaiser/chore/lint-using-putout
Halloween Lint using Putout (part 5)
2021-11-08 07:47:25 -08:00
Jörg Breitbart 1c5f24a707 Merge branch 'master' into osc_colors 2021-11-08 14:06:35 +01:00
Jörg Breitbart b7560be2b5 remove named colors 2021-11-08 14:03:36 +01:00
Jörg Breitbart 9d95c8c9c3 Merge branch 'master' into osc_colors 2021-11-07 15:51:30 +01:00
Simon Lamon 3582c00a61 Allow setting multiple options through term.options 2021-11-06 14:36:40 +00:00
coderaiser 8f1239674c Merge branch 'master' into chore/lint-using-putout 2021-11-05 21:38:30 +02:00
coderaiser 01b35ae4eb Update src/common/parser/Params.ts 2021-11-05 21:37:50 +02:00
coderaiser 82f8e06ff6 chore: lint using putout 2021-11-05 21:34:37 +02:00
Megan Rogge 1a8ac80cdf Merge pull request #3545 from meganrogge/merogge/release
v4.15.0
2021-11-05 13:17:11 -05:00
Megan Rogge b7c7fd9273 Merge branch 'master' into merogge/release 2021-11-05 13:08:28 -05:00
Megan Rogge 16641a5bb1 Revert "Revert "Avoid property use and float->number conversion""
This reverts commit b627f003e3.
2021-11-05 11:08:23 -07:00
Megan Rogge b7e5f0903b bump versions 2021-11-05 11:07:03 -07:00
Daniel Imms e40cf8043a Merge pull request #3544 from xtermjs/tyriar/length_warning
Add warning to IBufferLine.length
2021-11-05 09:09:37 -07:00
Daniel Imms 32cb95c7fb Add warning to IBufferLine.length 2021-11-05 08:53:48 -07:00
Eugeny f0303072f3 Merge branch 'master' into fix-duplicate-ime-input 2021-11-02 14:42:32 +01:00
jerch b2068531dd Merge pull request #3537 from jerch/update_deps
upgrade dev deps
2021-11-01 18:55:50 +01:00
Jörg Breitbart b29d7d2c8d update to windows-2019 on azure 2021-11-01 18:39:48 +01:00
Jörg Breitbart b684929d1e upgrade dev deps 2021-11-01 18:20:17 +01:00
Simon Lamon 47e53cbdaa Fix unit test calling eraseInDisplay instead of eraseInLine 2021-11-01 15:42:01 +00:00
Simon Lamon 086ac39095 Add whitespaces 2021-11-01 15:23:10 +00:00
Simon Lamon bec4f6d5c7 clear isWrapped in eraseBufferLine 2021-11-01 15:09:25 +00:00
Eugeny cfa0b30905 Merge branch 'master' into fix-duplicate-ime-input 2021-10-31 12:30:46 +01:00
Eugene Pankov 5a4e88dc51 input: prevent duplicate IME input on Linux - fixes #3533 2021-10-31 12:27:41 +01:00
Megan Rogge b627f003e3 Revert "Avoid property use and float->number conversion"
This reverts commit ffef3dba00.
2021-10-27 08:38:04 -07:00
Daniel Imms 07ba3dc197 Merge pull request #3532 from jeffg2k/patch-1
Check to make sure cols & rows are INT
2021-10-26 08:49:07 -07:00
ElJeffe 5c9adeef93 Check to make sure cols & rows are INT
I've noticed that if the terminal is hidden and the screen experiences a resize trigger from a blur / focus, it can cause the dimensions to be undefined.  This creates an exception in Xterm.js that the values are not integers.  Figured it would be ok to just check before doing a resize.
2021-10-26 11:44:52 -04:00
Daniel Imms 7abfccc23e Merge branch 'master' into master 2021-10-25 03:16:38 -07:00
Daniel Imms 394d7a2ee6 Merge pull request #3370 from dstein64/master
Set text baseline conditionally by browser.
2021-10-25 03:15:38 -07:00
Daniel Imms 987bb8dd35 Merge pull request #3476 from silamon/cleanbuild
Update npmignore files
2021-10-25 02:50:23 -07:00
Daniel Imms ce314844d2 Merge branch 'master' into cleanbuild 2021-10-25 02:47:52 -07:00
Jörg Breitbart 0871ace481 create minified color names module 2021-10-25 00:30:26 +02:00
Simon Lamon 645263b0ac update npmignore, don't include test files 2021-10-23 08:22:00 +00:00
Jörg Breitbart 5478dd2733 Merge branch 'master' into osc_colors 2021-10-23 00:57:17 +02:00
Jörg Breitbart 9eb72ee0cc report 16 bit color spec for query 2021-10-23 00:22:55 +02:00
Jörg Breitbart 258178fe85 cleanup & docs for OSC 10/11 2021-10-23 00:13:58 +02:00
Daniel Imms 6978fafd28 Merge pull request #3448 from silamon/options1
Expose typed options from OptionsService through terminal.options
2021-10-22 14:15:39 -07:00
Simon Lamon c7ee362ae7 revert changes to interface ITerminalOptions 2021-10-22 20:54:42 +00:00
Simon Lamon 57e6cfd500 setOption and getOption should look at publicOptions 2021-10-22 20:53:06 +00:00
Simon Lamon 33bbc4f4b8 cache options and type _options 2021-10-22 20:50:00 +00:00
Jörg Breitbart 01a732274c OSC 10/11 working, OSC 4 partially fixed 2021-10-22 22:48:57 +02:00
Daniel Imms a974d33c79 Merge branch 'master' into master 2021-10-22 13:14:11 -07:00
Daniel Imms 8549915afa Merge pull request #3525 from metonym/fix-viewport-scrollbar
Include xterm padding when computing viewport width
2021-10-22 13:09:57 -07:00
Daniel Steinberg 59f940c19c Set text baseline conditionally by browser. 2021-10-22 19:20:21 +00:00
metonym ed1aecb251 chore(viewport): run yarn lint --fix 2021-10-22 11:28:30 -07:00
metonym 7deb01267b Use computed padding of terminal element to compute viewport width 2021-10-22 11:02:59 -07:00
Daniel Imms 4b9ab81848 Add deprecated to getOption/setOption 2021-10-22 10:34:25 -07:00
Daniel Imms 6c8d801894 Adopt typed options in demo client 2021-10-22 10:14:03 -07:00
Daniel Imms 1a9de5f227 Merge branch 'master' into options1 2021-10-22 10:07:36 -07:00
metonym 9b1eec8ade fix(viewport): account for fallback scrollbar width 2021-10-22 09:49:01 -07:00
Jörg Breitbart 8e71cec208 remove only from tests 2021-10-22 18:32:30 +02:00
Jörg Breitbart 0f4d71b8f8 make linter happy 2021-10-22 18:26:46 +02:00
Jörg Breitbart 6a6043daa3 properly parse xcolor names 2021-10-22 18:17:52 +02:00
Daniel Imms 1a697bd844 Merge pull request #3479 from silamon/devcontainermocha
Add Mocha Test Explorer to devcontainer
2021-10-22 07:41:01 -07:00
jerch b23e100266 Merge pull request #3523 from jerch/fix_3347
properly await all parseP in inputhandler tests
2021-10-22 13:43:20 +02:00
Jörg Breitbart c9e8db38b6 properly await all parseP calls 2021-10-22 13:32:31 +02:00
Daniel Imms 0a7ac8b978 Merge pull request #3484 from JohanKnutzen/master
Add FleetDeck to real-world uses
2021-10-21 12:54:18 -07:00
Daniel Imms 780b48db53 Move newest to bottom of list 2021-10-21 12:54:01 -07:00
Daniel Imms 82d09ec909 Merge pull request #3522 from Tyriar/bump_release
Use Ubuntu 18.04 in release job
2021-10-21 11:44:13 -07:00
Daniel Imms b876639429 Use Ubuntu 18.04 in release job
Fixes #3521
2021-10-21 11:43:20 -07:00
jerch afe19e96fe simplify wheel handlers
- merge wheel handlers
- remove passive handler
2021-10-21 20:25:26 +02:00
jerch c599fbb7a1 Merge branch 'master' into fix_3518 2021-10-21 20:06:40 +02:00
Daniel Imms c63e9ef876 Merge pull request #3520 from meganrogge/cursor-blink
webgl - initialize CursorBlinkStateManager when CursorRenderLayer is created
2021-10-21 10:55:19 -07:00
Megan Rogge d6e828a2f3 fix merge conflict 2021-10-21 10:44:21 -07:00
Megan Rogge 2445f35a32 Merge branch 'master' into cursor-blink 2021-10-21 12:37:38 -05:00
Megan RoggeandDaniel Imms d2bcbc73d3 fixes #3517
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2021-10-21 10:33:22 -07:00
Jörg Breitbart f46ed82d7e simplify wheel handlers 2021-10-21 12:35:03 +02:00
Daniel Imms 4222dfb69d Merge pull request #3511 from Tyriar/windows_opener
Swallow error when opener can't be sent
2021-10-19 10:50:20 -07:00
Daniel Imms 1b3eb412b8 Merge pull request #3512 from Tyriar/serialize_doc
Call out deserialize into same size terminal
2021-10-19 10:50:10 -07:00
Daniel Imms e9f6636df1 Merge branch 'master' into serialize_doc 2021-10-19 10:49:47 -07:00
Daniel ImmsandMegan Rogge 472e410205 Call out deserialize into same size terminal
Fixes #3093

Co-authored-by: Megan Rogge <merogge@microsoft.com>
2021-10-19 10:49:01 -07:00
Daniel ImmsandMegan Rogge 581272ee51 Swallow error when opener can't be sent
Fixes #2943

Co-authored-by: Megan Rogge <merogge@microsoft.com>
2021-10-19 10:28:09 -07:00
Johan Knutzen e8e21e9d0b Merge branch 'master' into master 2021-10-18 11:02:13 -07:00
Daniel Imms 80d4742ded Merge pull request #3506 from Tyriar/2333_sendfocus
Report focus state on DECSET 1004
2021-10-12 11:58:43 -07:00
Daniel Imms 50939e0b1d Merge branch 'master' into 2333_sendfocus 2021-10-12 09:38:45 -07:00
Daniel Imms 5d5f34f65f Report focus state on DECSET 1004
Fixes #2333
2021-10-12 09:05:44 -07:00
jerch d3b762eb97 Merge pull request #3503 from jerch/fix_cursor_dom
fix DECTCEM in DOM renderer
2021-10-11 15:07:23 +02:00
Jörg Breitbart 0c60478953 fix DECTCEM in DOM renderer 2021-10-10 19:36:20 +02:00
Daniel Imms 3b23921a41 Merge pull request #3500 from meganrogge/merogge/safari-webgl
throw if activate webgl is called on safari
2021-10-08 08:00:53 -07:00
Megan Rogge e3422eda58 Merge branch 'master' into merogge/safari-webgl 2021-10-08 09:46:03 -05:00
Megan Rogge 37a4100220 throw if activate webgl called on safari 2021-10-08 07:45:48 -07:00
Daniel Imms 8592c5cca3 Merge pull request #3495 from MyunghoBae/master
Added goormIDE on Real-world uses
2021-10-07 06:50:35 -07:00
mac.bae 014530f582 Added goormIDE on Real-world uses 2021-10-07 07:44:30 +00:00
Johan Knutzen a994fbc865 Merge branch 'master' into master 2021-09-22 17:43:52 -07:00
Johan Knutzen 0d14336ec5 Add FleetDeck to real-world uses 2021-09-22 17:39:21 -07:00
Daniel Imms c20c07c6d8 Merge pull request #3483 from Tyriar/document_role
Add document role to accessibility tree root
2021-09-22 14:31:42 -07:00
Daniel Imms 3a71b3f11e Add document role to accessibility tree root
See microsoft/vscode#98918
2021-09-22 14:21:50 -07:00
Daniel Imms f2384694a1 Merge pull request #3481 from Tyriar/clear_texture_atlas
Add API to clear canvas renderer texture atlas
2021-09-22 06:51:56 -07:00
Daniel Imms ca46b8c805 Merge branch 'master' into clear_texture_atlas 2021-09-22 06:39:53 -07:00
Daniel Imms 189ff56224 Add API to clear canvas renderer texture atlas
Fixes #3455
2021-09-22 06:38:47 -07:00
Daniel Imms ef6516e561 Merge pull request #3480 from silamon/strikethrough-serialize
Support strikethrough in serialize addon
2021-09-22 05:22:40 -07:00
Simon Lamon 1d1fffdabc Include src files in distribution 2021-09-22 10:08:48 +00:00
Simon Lamon 3fe32c0724 Adjust test to include strikethrough test 2021-09-22 09:38:37 +00:00
Simon Lamon 2e4e29ad73 Support strikethrough in serialize addon 2021-09-22 08:47:36 +00:00
Simon Lamon 9c4a08a55a devcontainer mocha 2021-09-22 08:41:57 +00:00
Simon Lamon 5188633458 Update npmignore files 2021-09-15 17:39:39 +00:00
Daniel Imms a8a24eba51 Merge pull request #3473 from Tyriar/serialize_granularity
Add granularity to what the serialize addon serializes
2021-09-15 06:03:47 -07:00
Daniel Imms 6493edcddf Add new serialize option tests 2021-09-15 05:40:46 -07:00
Daniel Imms c186fbeafc Add exclude mode/alt buffer options to serialize addon
Fixes #3472
2021-09-15 05:04:11 -07:00
Simon Lamon 99239e9160 Allow modifying constructor only options 2021-09-09 18:02:40 +00:00
Simon Lamon 72a557cc1a Make options available in terminal 2021-09-01 07:59:45 +00:00
Daniel Imms 6bca6bdc1a Merge branch 'master' into master 2021-08-31 07:31:02 -07:00
Gerald db42dcfae2 fix: translate string offset to buffer index in wrapped line 2021-03-02 11:31:26 +08:00
Gerald ae037bdac0 fix: translate buffer col to string offset 2021-02-22 16:23:08 +08:00
Gerald 6f6378aaf2 fix length calculation of wide unicode chars 2021-02-22 16:06:57 +08:00
82 changed files with 2800 additions and 1808 deletions
+2 -1
View File
@@ -4,6 +4,7 @@
"appPort": 3000,
"extensions": [
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig"
"editorconfig.editorconfig",
"hbenl.vscode-mocha-test-adapter"
]
}
+8 -22
View File
@@ -2,10 +2,6 @@
*
!*/
# Whitelist - entries to be included must be negated with "!"
!*.js
!*.json
# Whitelist - css/
!css/**/*.css
@@ -32,25 +28,15 @@
!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
# Blacklist - (normal behavior) these will override any whitelist
*.d.ts.map
*.test.ts
*.test.d.ts
*.test.js
*.test.js.map
lib/test/
webpack.config.js
docs/
/.idea/
.vscode/
bin/
build/
fixtures/
demo/
.devcontainer/
out/
addons/
headless/
typings/xterm-headless.d.ts
+3 -1
View File
@@ -179,10 +179,12 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**WizardWebssh**](https://gitlab.com/mikeramsey/wizardwebssh): A terminal with Pyqt5 Widget for embedding, which can be used as an ssh client to connect to your ssh servers. It is written in Python, based on tornado, paramiko, and xterm.js.
- [**Wizard Assistant**](https://wizardassistant.com/): Wizard Assistant comes with advanced automation tools, preloaded common and special time-saving commands, and a built-in SSH terminal. Now you can remotely administer, troubleshoot, and analyze any system with ease.
- [**ucli**](https://github.com/tsadarsh/ucli): Command Line for everyone :family_man_woman_girl_boy: at [www.ucli.tech](https://www.ucli.tech).
- [**Tess**](https://github.com/SquitchYT/Tess/): Simple Terminal Fully Customizable for Everyone.
- [**Tess**](https://github.com/SquitchYT/Tess/): Simple Terminal Fully Customizable for Everyone. Discover more at [tessapp.dev](https://tessapp.dev)
- [**HashiCorp Nomad**](https://www.nomadproject.io/): A container orchestrator with the ability to connect to remote tasks via a web interface using websockets and xterm.js.
- [**TermPair**](https://github.com/cs01/termpair): View and control terminals from your browser with end-to-end encryption
- [**gdbgui**](https://github.com/cs01/gdbgui): Browser-based frontend to gdb (gnu debugger)
- [**goormIDE**](https://ide.goorm.io/): Run almost every programming languages with real-time collaboration, live pair programming, and built-in messenger.
- [**FleetDeck**](https://fleetdeck.io): Remote desktop & virtual terminal
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
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.
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
+1 -1
View File
@@ -33,7 +33,7 @@ export class FitAddon implements ITerminalAddon {
public fit(): void {
const dims = this.proposeDimensions();
if (!dims || !this._terminal) {
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
return;
}
+10 -29
View File
@@ -2,21 +2,13 @@
*
!*/
# Whitelist - entries to be included must be negated with "!"
!*.js
!*.json
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
# Whitelist - out/
!out/**/*.d.ts
!out/**/*.js
!out/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
@@ -25,24 +17,13 @@
!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
# Blacklist - (normal behavior) these will override any whitelist
*.test.ts
*.test.d.ts
*.test.js
*.test.js.map
docs/
/.idea/
.vscode/
coverage/
.nyc_output/
fonts/
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
!typings/*.d.ts
+2 -2
View File
@@ -43,7 +43,7 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
if (status && status.state !== 'granted') {
throw new Error('Permission to access local fonts not granted.');
}
} catch (err) {
} catch (err: any) {
// A `TypeError` indicates the 'local-fonts'
// permission is not yet implemented, so
// only `throw` if this is _not_ the problem.
@@ -61,7 +61,7 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
fonts[metadata.family].push(metadata);
}
fontsPromise = Promise.resolve(fonts);
} catch (err) {
} catch (err: any) {
console.error(err.name, err.message);
}
}
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-search",
"version": "0.8.1",
"version": "0.8.2",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+118 -54
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
export interface ISearchOptions {
regex?: boolean;
@@ -21,8 +21,20 @@ export interface ISearchResult {
term: string;
col: number;
row: number;
size: number;
}
type LineCacheEntry = [
/**
* The string representation of a line (as opposed to the buffer cell representation).
*/
lineAsString: string,
/**
* The offsets where each line starts when the entry describes a wrapped line.
*/
lineOffsets: number[]
];
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
@@ -34,7 +46,7 @@ export class SearchAddon implements ITerminalAddon {
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: string[] | undefined;
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeoutId = 0;
private _cursorMoveListener: IDisposable | undefined;
private _resizeListener: IDisposable | undefined;
@@ -257,7 +269,7 @@ export class SearchAddon implements ITerminalAddon {
*/
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!;
let row = searchPosition.startRow;
const row = searchPosition.startRow;
const col = searchPosition.startCol;
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
@@ -274,14 +286,16 @@ export class SearchAddon implements ITerminalAddon {
searchPosition.startCol += terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
}
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) {
stringLine = this._translateBufferLineToStringWithWrap(row, true);
let cache = this._linesCache?.[row];
if (!cache) {
cache = this._translateBufferLineToStringWithWrap(row, true);
if (this._linesCache) {
this._linesCache[row] = stringLine;
this._linesCache[row] = cache;
}
}
const [stringLine, offsets] = cache;
const offset = this._bufferColsToStringOffset(row, col);
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
@@ -290,68 +304,108 @@ export class SearchAddon implements ITerminalAddon {
const searchRegex = RegExp(searchTerm, 'g');
let foundTerm: RegExpExecArray | null;
if (isReverseSearch) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..col
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
term = foundTerm[0];
searchRegex.lastIndex -= (term.length - 1);
}
} else {
foundTerm = searchRegex.exec(searchStringLine.slice(col));
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
if (foundTerm && foundTerm[0].length > 0) {
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
term = foundTerm[0];
}
}
} else {
if (isReverseSearch) {
if (col - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
if (offset - searchTerm.length >= 0) {
resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
}
} else {
resultIndex = searchStringLine.indexOf(searchTerm, col);
resultIndex = searchStringLine.indexOf(searchTerm, offset);
}
}
if (resultIndex >= 0) {
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
if (resultIndex >= terminal.cols) {
row += Math.floor(resultIndex / terminal.cols);
resultIndex = resultIndex % terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
return;
}
const line = terminal.buffer.active.getLine(row);
if (line) {
for (let i = 0; i < resultIndex; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.getChars();
if (char.length > 1) {
resultIndex -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const charWidth = cell.getWidth();
if (charWidth === 0) {
resultIndex++;
}
}
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
let startRowOffset = 0;
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
startRowOffset++;
}
let endRowOffset = startRowOffset;
while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
endRowOffset++;
}
const startColOffset = resultIndex - offsets[startRowOffset];
const endColOffset = resultIndex + term.length - offsets[endRowOffset];
const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset);
return {
term,
col: resultIndex,
row
col: startColIndex,
row: row + startRowOffset,
size
};
}
}
private _stringLengthToBufferSize(row: number, offset: number): number {
const line = this._terminal!.buffer.active.getLine(row);
if (!line) {
return 0;
}
for (let i = 0; i < offset; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.getChars();
if (char.length > 1) {
offset -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const nextCell = line.getCell(i + 1);
if (nextCell && nextCell.getWidth() === 0) {
offset++;
}
}
return offset;
}
private _bufferColsToStringOffset(startRow: number, cols: number): number {
const terminal = this._terminal!;
let lineIndex = startRow;
let offset = 0;
let line = terminal.buffer.active.getLine(lineIndex);
while (cols > 0 && line) {
for (let i = 0; i < cols && i < terminal.cols; i++) {
const cell = line.getCell(i);
if (!cell) {
break;
}
if (cell.getWidth()) {
offset += cell.getChars().length;
}
}
lineIndex++;
line = terminal.buffer.active.getLine(lineIndex);
if (line && !line.isWrapped) {
break;
}
cols -= terminal.cols;
}
return offset;
}
/**
* Translates a buffer line to a string, including subsequent lines if they are wraps.
* Wide characters will count as two columns in the resulting string. This
@@ -360,23 +414,33 @@ export class SearchAddon implements ITerminalAddon {
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
*/
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
const terminal = this._terminal!;
let lineString = '';
let lineWrapsToNext: boolean;
do {
const strings = [];
const lineOffsets = [0];
let line = terminal.buffer.active.getLine(lineIndex);
while (line) {
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
const line = terminal.buffer.active.getLine(lineIndex);
if (!line) {
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
let string = line.translateToString(!lineWrapsToNext && trimRight);
if (lineWrapsToNext && nextLine) {
const lastCell = line.getCell(line.length - 1);
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
// a wide character wrapped to the next line
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
string = string.slice(0, -1);
}
}
strings.push(string);
if (lineWrapsToNext) {
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
} else {
break;
}
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
lineIndex++;
} while (lineWrapsToNext);
return lineString;
line = nextLine;
}
return [strings.join(''), lineOffsets];
}
/**
@@ -390,7 +454,7 @@ export class SearchAddon implements ITerminalAddon {
terminal.clearSelection();
return false;
}
terminal.select(result.col, result.row, result.term.length);
terminal.select(result.col, result.row, result.size);
// If it is not in the viewport then we scroll else it just gets selected
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
let scroll = result.row - terminal.buffer.active.viewportY;
@@ -103,6 +103,23 @@ describe('Search Tests', function(): void {
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
});
it('Search for result bounding with wide unicode chars', async () => {
await writeSync(page, '中文xx𝄞𝄞');
assert.deepEqual(await page.evaluate(`window.search.findNext('中')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '中');
assert.deepEqual(await page.evaluate(`window.search.findNext('xx')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'xx');
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
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
});
});
describe('Regression tests', () => {
describe('#2444 wrapped line content not being found', () => {
let fixture: string;
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
@@ -14,7 +14,7 @@
"SerializeAddon": ["../src/SerializeAddon"]
}
},
"include": ["../**/*", "../../../typings/xterm.d.ts", "../../../out/**/*"],
"include": ["../**/*", "../../../typings/xterm.d.ts"],
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
"references": [
{ "path": "../../../src/common" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-serialize",
"version": "0.6.0",
"version": "0.6.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -72,7 +72,8 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
&& cell1.isItalic() === cell2.isItalic()
&& cell1.isDim() === cell2.isDim();
&& cell1.isDim() === cell2.isDim()
&& cell1.isStrikethrough() === cell2.isStrikethrough();
}
class StringSerializeHandler extends BaseSerializeHandler {
@@ -160,7 +161,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (
// you must output character to cause overflow, control sequence can't do this
nextRowFirstChar.getChars() &&
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
) {
if (
// the last character can't be null,
@@ -259,6 +260,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }
if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }
}
}
}
@@ -433,26 +435,37 @@ export class SerializeAddon implements ITerminalAddon {
return content;
}
public serialize(scrollback?: number): string {
public serialize(options?: ISerializeOptions): string {
// TODO: Add combinedData support
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
// Normal buffer
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, scrollback);
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
// Alternate buffer
if (this._terminal.buffer.active.type === 'alternate') {
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
if (!options?.excludeAltBuffer) {
if (this._terminal.buffer.active.type === 'alternate') {
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
}
}
// Modes
content += this._serializeModes(this._terminal);
if (!options?.excludeModes) {
content += this._serializeModes(this._terminal);
}
return content;
}
public dispose(): void { }
}
interface ISerializeOptions {
scrollback?: number;
excludeModes?: boolean;
excludeAltBuffer?: boolean;
}
@@ -146,7 +146,7 @@ describe('SerializeAddon', () => {
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfScrollback});`), lines.slice(halfScrollback, rows).join('\r\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
});
it('serialize 0 rows of scrollback', async function(): Promise<any> {
@@ -154,7 +154,19 @@ describe('SerializeAddon', () => {
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), lines.slice(rows - 10, rows).join('\r\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n'));
});
it('serialize exclude modes', async () => {
await writeSync(page, 'before\\x1b[?1hafter');
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h');
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter');
});
it('serialize exclude alt buffer', async () => {
await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt');
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt');
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal');
});
it('serialize all rows of content with color16', async function(): Promise<any> {
@@ -184,11 +196,13 @@ describe('SerializeAddon', () => {
sgr(UNDERLINED) + line,
sgr(BLINK) + line,
sgr(INVISIBLE) + line,
sgr(STRIKETHROUGH) + line,
sgr(NO_INVERSE) + line,
sgr(NO_BOLD) + line,
sgr(NO_UNDERLINED) + line,
sgr(NO_BLINK) + line,
sgr(NO_INVISIBLE) + line
sgr(NO_INVISIBLE) + line,
sgr(NO_STRIKETHROUGH) + line
];
const rows = lines.length;
await writeSync(page, lines.join('\\r\\n'));
@@ -579,20 +593,20 @@ const BG_RGB_GREEN = '48;2;0;255;0';
const BG_RGB_YELLOW = '48;2;255;255;0';
const BG_RESET = '49';
const INVERSE = '7';
const BOLD = '1';
const DIM = '2';
const ITALIC = '3';
const UNDERLINED = '4';
const BLINK = '5';
const INVERSE = '7';
const INVISIBLE = '8';
const STRIKETHROUGH = '9';
const NO_INVERSE = '27';
const NO_BOLD = '22';
const NO_DIM = '22';
const NO_ITALIC = '23';
const NO_UNDERLINED = '24';
const NO_BLINK = '25';
const NO_INVERSE = '27';
const NO_INVISIBLE = '28';
const ITALIC = '3';
const DIM = '2';
const NO_ITALIC = '23';
const NO_DIM = '22';
const NO_STRIKETHROUGH = '29';
@@ -7,14 +7,14 @@ import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-serialize' {
/**
* An xterm.js addon that enables web links.
* An xterm.js addon that enables serialization of terminal contents.
*/
export class SerializeAddon implements ITerminalAddon {
constructor();
/**
* Activates the addon
* Activates the addon.
* @param terminal The terminal the addon is being loaded in.
*/
public activate(terminal: Terminal): void;
@@ -24,15 +24,36 @@ declare module 'xterm-addon-serialize' {
* the state. The cursor will also be positioned to the correct cell. When restoring a terminal
* it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering
* incomplete frames.
* @param scrollback The number of rows in scrollback buffer to serialize, starting from the
* bottom of the scrollback buffer. This defaults to the all available rows in the scrollback
* buffer.
*
* It's recommended that you write the serialized data into a terminal of the same size in which
* it originated from and then resize it after if needed.
*
* @param options Custom options to allow control over what gets serialized.
*/
public serialize(scrollback?: number): string;
public serialize(options?: ISerializeOptions): string;
/**
* Disposes the addon.
*/
public dispose(): void;
}
export interface ISerializeOptions {
/**
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
* serialized.
*/
scrollback?: number;
/**
* Whether to exclude the terminal modes from the serialization. False by default.
*/
excludeModes?: boolean;
/**
* Whether to exclude the alt buffer from the serialization. False by default.
*/
excludeAltBuffer?: boolean;
}
}
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
+29 -5
View File
@@ -1,5 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
"version": "0.4.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -29,7 +29,11 @@ const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
const newWindow = window.open();
if (newWindow) {
newWindow.opener = null;
try {
newWindow.opener = null;
} catch {
// no-op, Electron can throw
}
newWindow.location.href = uri;
} else {
console.warn('Opening link blocked as opener could not be cleared');
@@ -39,6 +43,7 @@ function handleLink(event: MouseEvent, uri: string): void {
interface ILinkProviderOptions {
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
leave?(event: MouseEvent, text: string): void;
urlRegex?: RegExp;
}
export class WebLinksAddon implements ITerminalAddon {
@@ -58,7 +63,8 @@ export class WebLinksAddon implements ITerminalAddon {
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
const options = this._options as ILinkProviderOptions;
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler, options));
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;
+29 -7
View File
@@ -1,7 +1,29 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
out-test/
test/
# 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.11.2",
"version": "0.11.4",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -8,6 +8,7 @@ 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';
export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
@@ -23,6 +24,9 @@ export class WebglAddon implements ITerminalAddon {
if (!terminal.element) {
throw new Error('Cannot activate WebglAddon before Terminal.open');
}
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;
@@ -60,7 +60,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._renderLayers = [
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
];
this.dimensions = {
scaledCharWidth: 0,
@@ -4,7 +4,7 @@
*/
import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY } from 'browser/renderer/atlas/Constants';
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';
@@ -14,8 +14,8 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { channels, rgba } from 'browser/Color';
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
// 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;
@@ -368,7 +368,7 @@ export class WebglCharAtlas implements IDisposable {
const fontStyle = italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'ideographic';
this._tmpCtx.textBaseline = TEXT_BASELINE;
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
@@ -463,7 +463,7 @@ export class WebglCharAtlas implements IDisposable {
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 + this._config.scaledCharWidth > TEXTURE_WIDTH) {
if (this._currentRowX + rasterizedGlyph.size.x > TEXTURE_WIDTH) {
this._currentRowX = 0;
this._currentRowY += this._currentRowHeight;
this._currentRowHeight = 0;
@@ -7,6 +7,7 @@ import { IRenderLayer } from './Types';
import { acquireCharAtlas } from '../atlas/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 { CellData } from 'common/buffer/CellData';
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
@@ -224,7 +225,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'ideographic';
this._ctx.textBaseline = TEXT_BASELINE;
this._clipRow(terminal, y);
this._ctx.fillText(
cell.getChars(),
@@ -31,6 +31,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cell: ICellData = new CellData();
constructor(
terminal: Terminal,
container: HTMLElement,
zIndex: number,
colors: IColorSet,
@@ -50,7 +51,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
this.onOptionsChanged(terminal);
}
public resize(terminal: Terminal, dim: IRenderDimensions): void {
@@ -67,25 +68,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(terminal: Terminal): void {
this._clearCursor();
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this.onOptionsChanged(terminal);
}
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
this.onOptionsChanged(terminal);
}
public onBlur(terminal: Terminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
this._cursorBlinkStateManager?.pause();
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public onFocus(terminal: Terminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume(terminal);
} else {
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
this._cursorBlinkStateManager?.resume(terminal);
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
}
public onOptionsChanged(terminal: Terminal): void {
@@ -105,9 +99,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onCursorMove(terminal: Terminal): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
}
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
@@ -302,6 +294,7 @@ class CursorBlinkStateManager {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
// Setup the initial timeout which will hide the cursor, this is done before
+3 -3
View File
@@ -59,7 +59,7 @@ jobs:
- job: Windows
pool:
vmImage: 'vs2017-win2016'
vmImage: 'windows-2019'
steps:
- task: NodeTool@0
inputs:
@@ -124,7 +124,7 @@ jobs:
- job: Windows_IntegrationTests
pool:
vmImage: 'vs2017-win2016'
vmImage: 'windows-2019'
steps:
- task: NodeTool@0
inputs:
@@ -147,7 +147,7 @@ jobs:
- Windows_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
vmImage: 'ubuntu-16.04'
vmImage: 'ubuntu-18.04'
steps:
- task: NodeTool@0
inputs:
+3 -3
View File
@@ -11,13 +11,13 @@ const Mustache = require('mustache');
* regexp to fetch all comments
* Fetches all multiline comments and single lines containing '// @vt:'.
*/
const REX_COMMENTS = /^\s*?[/][*][*]([\s\S]*?)[*][/]|^\s*?\/\/ ([@]vt[:].*?)$/mug;
const REX_COMMENTS = /^\s*?\/\*\*([\S\s]*?)\*\/|^\s*?\/\/ (@vt:.*?)$/mug;
/**
* regexp to parse the @vt line
* expected data - "@vt: <status> <kind> <mnemonic> "<name>" "<sequence>" "<short description>"
*/
const REX_VT_LINE = /^[@]vt\:\s*(\w+|#\w+|#\w+\[.*?\])\s*(\w+)\s*(\w+)\s*"(.*?)"\s*"(.*?)"\s*"(.*?)".*$/;
const REX_VT_LINE = /^@vt:\s*(\w+|#\w+|#\w+\[.*?\])\s*(\w+)\s*(\w+)\s*"(.*?)"\s*"(.*?)"\s*"(.*?)".*$/;
// known vt command types
const TYPES = [
@@ -362,7 +362,7 @@ function* parseMultiLineGen(filename, s) {
if (!s.includes('@vt:')) {
return;
}
const lines = s.split('\n').map(el => el.trim().replace(/[*]/, '').replace(/\s/, ''));
const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, ''));
let grabLine = false;
let longDescription = [];
let feature = undefined;
+4 -4
View File
@@ -92,7 +92,7 @@ function checkAndPublishPackage(packageDir) {
}
function getNextBetaVersion(packageJson) {
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
if (!/^\d+\.\d+\.\d+$/.exec(packageJson.version)) {
console.error('The package.json version must be of the form x.y.z');
process.exit(1);
}
@@ -104,11 +104,11 @@ function getNextBetaVersion(packageJson) {
return `${nextStableVersion}-${tag}.1`;
}
const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
const bVersion = parseInt(b.substr(b.search(/[0-9]+$/)));
const aVersion = parseInt(a.substr(a.search(/\d+$/)));
const bVersion = parseInt(b.substr(b.search(/\d+$/)));
return aVersion > bVersion ? -1 : 1;
})[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10);
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
}
+2 -2
View File
@@ -21,7 +21,7 @@ let flagArgs = [];
if (process.argv.length > 2) {
const args = process.argv.slice(2);
flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat([...val], []));
flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat(val.slice(), []));
console.info(flagArgs);
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
@@ -44,7 +44,7 @@ const server = cp.spawn('node', ['demo/start'], {
server.stdout.on('data', (data) => {
// await for the server to fully start
if (data.indexOf("successfully") !== -1) {
if (data.includes("successfully")) {
const run = cp.spawnSync(
npmBinScript('mocha'),
[...testFiles, ...flagArgs], {
+2 -1
View File
@@ -133,7 +133,8 @@
cursor: default;
}
.xterm.xterm-cursor-pointer {
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
+15 -12
View File
@@ -293,11 +293,11 @@ function initOptions(term: TerminalType): void {
rendererType: ['dom', 'canvas'],
wordSeparator: null
};
const options = Object.keys((<any>term)._core.options);
const options = Object.getOwnPropertyNames(term.options);
const booleanOptions = [];
const numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.getOption(o)) {
switch (typeof term.options[o]) {
case 'boolean':
booleanOptions.push(o);
break;
@@ -314,18 +314,18 @@ function initOptions(term: TerminalType): void {
let html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o]}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (stringOptions[o]) {
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.options[o] === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
}
});
html += '</div>';
@@ -338,7 +338,7 @@ function initOptions(term: TerminalType): void {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
term.options[o] = input.checked;
});
});
numberOptions.forEach(o => {
@@ -347,14 +347,17 @@ function initOptions(term: TerminalType): void {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else if (o === 'lineHeight' || o === 'scrollSensitivity') {
term.setOption(o, parseFloat(input.value));
} else if (o === 'lineHeight') {
term.options.lineHeight = parseFloat(input.value);
updateTerminalSize();
} else if (o === 'scrollSensitivity') {
term.options.scrollSensitivity = parseFloat(input.value);
updateTerminalSize();
} else if(o === 'scrollback') {
term.setOption(o, parseInt(input.value));
term.options.scrollback = parseInt(input.value);
setTimeout(() => updateTerminalSize(), 5);
} else {
term.setOption(o, parseInt(input.value));
term.options[o] = parseInt(input.value);
}
});
});
@@ -362,7 +365,7 @@ function initOptions(term: TerminalType): void {
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
term.options[o] = input.value;
});
});
}
+28 -5
View File
@@ -1,6 +1,29 @@
# Include
!typings/*.d.ts
!lib-headless/
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Exclude
test/
# Whitelist - lib-headless/
!lib-headless/**/*.d.ts
!lib-headless/**/*.js
!lib-headless/**/*.js.map
!lib-headless/**/*.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
+23 -23
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.14.1",
"version": "4.16.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -37,39 +37,39 @@
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts"
},
"devDependencies": {
"@types/chai": "^4.2.18",
"@types/debug": "^4.1.5",
"@types/chai": "^4.2.22",
"@types/debug": "^4.1.7",
"@types/deep-equal": "^1.0.1",
"@types/glob": "^7.1.3",
"@types/jsdom": "^16.2.10",
"@types/mocha": "^8.2.2",
"@types/glob": "^7.2.0",
"@types/jsdom": "^16.2.13",
"@types/mocha": "^9.0.0",
"@types/node": "^14.14.44",
"@types/utf8": "^2.1.6",
"@types/utf8": "^3.0.0",
"@types/webpack": "^5.28.0",
"@types/ws": "^7.4.4",
"@typescript-eslint/eslint-plugin": "^4.23.0",
"@typescript-eslint/parser": "^4.23.0",
"@types/ws": "^8.2.0",
"@typescript-eslint/eslint-plugin": "^5.3.0",
"@typescript-eslint/parser": "^5.3.0",
"chai": "^4.3.4",
"cross-env": "^7.0.3",
"deep-equal": "^2.0.5",
"eslint": "^7.26.0",
"eslint": "^8.1.0",
"express": "^4.17.1",
"express-ws": "^4.0.0",
"glob": "^7.1.7",
"jsdom": "^16.5.3",
"mocha": "^8.4.0",
"express-ws": "^5.0.2",
"glob": "^7.2.0",
"jsdom": "^18.0.1",
"mocha": "^9.1.3",
"mustache": "^4.2.0",
"node-pty": "^0.10.1",
"nyc": "^15.1.0",
"playwright": "^1.11.0",
"source-map-loader": "^2.0.1",
"source-map-support": "^0.5.19",
"playwright": "^1.16.2",
"source-map-loader": "^3.0.0",
"source-map-support": "^0.5.20",
"ts-loader": "^9.1.2",
"typescript": "^4.2.4",
"typescript": "^4.4.4",
"utf8": "^3.0.0",
"webpack": "^5.37.0",
"webpack-cli": "^4.7.0",
"ws": "^7.4.5",
"xterm-benchmark": "^0.2.1"
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1",
"ws": "^8.2.3",
"xterm-benchmark": "^0.3.0"
}
}
+2
View File
@@ -53,7 +53,9 @@ export class AccessibilityManager extends Disposable {
) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.setAttribute('role', 'document');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._accessibilityTreeRoot.tabIndex = 0;
this._rowContainer = document.createElement('div');
this._rowContainer.setAttribute('role', 'list');
+8
View File
@@ -4,6 +4,7 @@
*/
import { IColor } from 'browser/Types';
import { IColorRGB } from 'common/Types';
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
@@ -17,6 +18,8 @@ export namespace channels {
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32
// on LE systems, before it can be used for direct 32-bit buffer writes.
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
@@ -81,6 +84,10 @@ export namespace color {
rgba: channels.toRgba(r, g, b, a)
};
}
export function toColorRGB(color: IColor): IColorRGB {
return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];
}
}
/**
@@ -197,6 +204,7 @@ export namespace rgba {
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
// FIXME: Move this to channels NS?
export function toChannels(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
+45
View File
@@ -7,6 +7,16 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/T
import { ITheme } from 'common/services/Services';
import { channels, color, css } from 'browser/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
import { ColorIndex } from 'common/Types';
interface IRestoreColorSet {
foreground: IColor;
background: IColor;
cursor: IColor;
ansi: IColor[];
}
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
const DEFAULT_BACKGROUND = css.toColor('#000000');
@@ -73,6 +83,7 @@ export class ColorManager implements IColorManager {
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
private _contrastCache: IColorContrastCache;
private _restoreColors!: IRestoreColorSet;
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
@@ -96,6 +107,7 @@ export class ColorManager implements IColorManager {
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
this._updateRestoreColors();
}
public onOptionsChange(key: string): void {
@@ -142,6 +154,39 @@ export class ColorManager implements IColorManager {
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
// Clear our the cache
this._contrastCache.clear();
this._updateRestoreColors();
}
public restoreColor(slot?: ColorIndex): void {
// unset slot restores all ansi colors
if (slot === undefined) {
for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
this.colors.ansi[i] = this._restoreColors.ansi[i];
}
return;
}
switch (slot) {
case ColorIndex.FOREGROUND:
this.colors.foreground = this._restoreColors.foreground;
break;
case ColorIndex.BACKGROUND:
this.colors.background = this._restoreColors.background;
break;
case ColorIndex.CURSOR:
this.colors.cursor = this._restoreColors.cursor;
break;
default:
this.colors.ansi[slot] = this._restoreColors.ansi[slot];
}
}
private _updateRestoreColors(): void {
this._restoreColors = {
foreground: this.colors.foreground,
background: this.colors.background,
cursor: this.colors.cursor,
ansi: [...this.colors.ansi]
};
}
private _parseColor(
+17 -1
View File
@@ -11,7 +11,7 @@ import { IBufferService, IUnicodeService } from 'common/services/Services';
import { Linkifier } from 'browser/Linkifier';
import { MockLogService, MockUnicodeService } from 'common/TestUtils.test';
import { IRegisteredLinkMatcher, IMouseZoneManager, IMouseZone } from 'browser/Types';
import { IMarker } from 'common/Types';
import { IMarker, ITerminalOptions } from 'common/Types';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -1501,6 +1501,22 @@ describe('Terminal', () => {
assert.deepEqual(markers.map(el => el.line), [-1, -1, 0, 1, 2]);
});
});
describe('options', () => {
beforeEach(async () => {
term = new TestTerminal({});
});
it('get options', () => {
assert.equal(term.options.cols, 80);
assert.equal(term.options.rows, 24);
});
it('set options', async () => {
term.options.cols = 40;
assert.equal(term.options.cols, 40);
term.options.rows = 20;
assert.equal(term.options.rows, 20);
});
});
});
class TestLinkifier extends Linkifier {
+100 -46
View File
@@ -39,7 +39,7 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IAnsiColorChangeEvent } from 'common/Types';
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
@@ -52,9 +52,9 @@ import { MouseService } from 'browser/services/MouseService';
import { Linkifier2 } from 'browser/Linkifier2';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { CoreTerminal } from 'common/CoreTerminal';
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
import { rgba } from 'browser/Color';
import { color, rgba } from 'browser/Color';
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
import { toRgbString } from 'common/input/XParseColor';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -74,9 +74,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
public browser: IBrowser = Browser as any;
// TODO: We should remove options once components adopt optionsService
public get options(): IInitializedTerminalOptions { return this.optionsService.options; }
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
// browser services
@@ -152,7 +149,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
* @alias module:xterm/src/xterm
*/
constructor(
options: ITerminalOptions = {}
options: Partial<ITerminalOptions> = {}
) {
super(options);
@@ -164,9 +161,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this.bell()));
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus()));
this.register(this._inputHandler.onRequestReset(() => this.reset()));
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
this.register(this._inputHandler.onColor((event) => this._handleColorEvent(event)));
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange));
this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
@@ -176,17 +174,55 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
}
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
if (!this._colorManager) { return; }
for (const ansiColor of event.colors) {
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
/**
* Handle color event from inputhandler for OSC 4|104 | 10|110 | 11|111 | 12|112.
* An event from OSC 4|104 may contain multiple set or report requests, and multiple
* or none restore requests (resetting all),
* while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request.
*/
private _handleColorEvent(event: IColorEvent): void {
if (!this._colorManager) return;
for (const req of event) {
let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
let ident = '';
switch (req.index) {
case ColorIndex.FOREGROUND: // OSC 10 | 110
acc = 'foreground';
ident = '10';
break;
case ColorIndex.BACKGROUND: // OSC 11 | 111
acc = 'background';
ident = '11';
break;
case ColorIndex.CURSOR: // OSC 12 | 112
acc = 'cursor';
ident = '12';
break;
default: // OSC 4 | 104
// we can skip the [0..255] range check here (already done in inputhandler)
acc = 'ansi';
ident = '4;' + req.index;
}
if (acc) {
switch (req.type) {
case ColorRequestType.REPORT:
const channels = color.toColorRGB(acc === 'ansi'
? this._colorManager.colors.ansi[req.index]
: this._colorManager.colors[acc]);
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`);
break;
case ColorRequestType.SET:
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
break;
case ColorRequestType.RESTORE:
this._colorManager.restoreColor(req.index);
break;
}
}
}
this._renderService?.setColors(this._colorManager!.colors);
this.viewport?.onThemeChange(this._colorManager!.colors);
this._renderService?.setColors(this._colorManager.colors);
this.viewport?.onThemeChange(this._colorManager.colors);
}
public dispose(): void {
@@ -493,7 +529,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.viewport = this._instantiationService.createInstance(Viewport,
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
this._viewportElement,
this._viewportScrollArea
this._viewportScrollArea,
this.element
);
this.viewport.onThemeChange(this._colorManager.colors);
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
@@ -535,7 +572,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this._mouseZoneManager);
this.register(this.onScroll(() => this._mouseZoneManager!.clearAll()));
this.linkifier.attachToDom(this.element, this._mouseZoneManager);
this.linkifier2.attachToDom(this.element, this._mouseService, this._renderService);
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
// This event listener must be registered aftre MouseZoneManager is created
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
@@ -698,8 +735,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
},
wheel: (ev: WheelEvent) => {
sendEvent(ev);
ev.preventDefault();
return this.cancel(ev);
return this.cancel(ev, true);
},
mousedrag: (ev: MouseEvent) => {
// deal only with move while a button is held
@@ -794,34 +830,32 @@ export class Terminal extends CoreTerminal implements ITerminal {
}));
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (!requestedEvents.wheel) {
// do nothing, if app side handles wheel itself
if (requestedEvents.wheel) return;
if (!this.buffer.hasScrollback) {
// Convert wheel events into up/down events when the buffer does not have scrollback, this
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
if (!this.buffer.hasScrollback) {
const amount = this.viewport!.getLinesScrolled(ev);
const amount = this.viewport!.getLinesScrolled(ev);
// Do nothing if there's no vertical scroll
if (amount === 0) {
return;
}
// Construct and send sequences
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
let data = '';
for (let i = 0; i < Math.abs(amount); i++) {
data += sequence;
}
this.coreService.triggerDataEvent(data, true);
// Do nothing if there's no vertical scroll
if (amount === 0) {
return;
}
return;
}
}, { passive: true }));
// allow wheel scrolling in
// the shell for example
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
if (requestedEvents.wheel) return;
if (!this.viewport!.onWheel(ev)) {
// Construct and send sequences
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
let data = '';
for (let i = 0; i < Math.abs(amount); i++) {
data += sequence;
}
this.coreService.triggerDataEvent(data, true);
return this.cancel(ev, true);
}
// normal viewport scrolling
// conditionally stop event, if the viewport still had rows to scroll within
if (this.viewport!.onWheel(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
@@ -1170,6 +1204,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
this._keyPressHandled = true;
// The key was handled so clear the dead key state, otherwise certain keystrokes like arrow
// keys could be ignored
this._unprocessedDeadKey = false;
return true;
}
@@ -1182,11 +1220,15 @@ export class Terminal extends CoreTerminal implements ITerminal {
protected _inputEvent(ev: InputEvent): boolean {
// Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to
// support reading out character input which can doubling up input characters
if (ev.data && ev.inputType === 'insertText' && !this.optionsService.options.screenReaderMode) {
if (ev.data && ev.inputType === 'insertText' && !ev.composed && !this.optionsService.options.screenReaderMode) {
if (this._keyPressHandled) {
return false;
}
// The key was handled so clear the dead key state, otherwise certain keystrokes like arrow
// keys could be ignored
this._unprocessedDeadKey = false;
const text = ev.data;
this.coreService.triggerDataEvent(text, true);
@@ -1292,6 +1334,18 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.viewport?.syncScrollArea();
}
public clearTextureAtlas(): void {
this._renderService?.clearTextureAtlas();
}
private _reportFocus(): void {
if (this.element?.classList.contains('focus')) {
this.coreService.triggerDataEvent(C0.ESC + '[I');
} else {
this.coreService.triggerDataEvent(C0.ESC + '[O');
}
}
private _reportWindowsOptions(type: WindowsOptionsReportType): void {
if (!this._renderService) {
return;
+6
View File
@@ -195,6 +195,9 @@ export class MockTerminal implements ITerminal {
public reset(): void {
throw new Error('Method not implemented.');
}
public clearTextureAtlas(): void {
throw new Error('Method not implemented.');
}
public refresh(start: number, end: number): void {
throw new Error('Method not implemented.');
}
@@ -374,6 +377,9 @@ export class MockRenderService implements IRenderService {
public refreshRows(start: number, end: number): void {
throw new Error('Method not implemented.');
}
public clearTextureAtlas(): void {
throw new Error('Method not implemented.');
}
public resize(cols: number, rows: number): void {
throw new Error('Method not implemented.');
}
+1 -1
View File
@@ -16,7 +16,6 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal {
browser: IBrowser;
buffer: IBuffer;
viewport: IViewport | undefined;
// TODO: We should remove options once components adopt optionsService
options: ITerminalOptions;
linkifier: ILinkifier;
linkifier2: ILinkifier2;
@@ -79,6 +78,7 @@ export interface IPublicTerminal extends IDisposable {
write(data: string | Uint8Array, callback?: () => void): void;
paste(data: string): void;
refresh(start: number, end: number): void;
clearTextureAtlas(): void;
reset(): void;
}
+6 -3
View File
@@ -42,6 +42,7 @@ export class Viewport extends Disposable implements IViewport {
private readonly _scrollLines: (amount: number) => void,
private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement,
private readonly _element: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@@ -116,7 +117,9 @@ export class Viewport extends Disposable implements IViewport {
}
this._lastHadScrollBar = this.scrollBarWidth > 0;
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth).toString() + 'px';
const elementStyle = window.getComputedStyle(this._element);
const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight);
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px';
this._refreshAnimationFrame = null;
}
@@ -217,7 +220,7 @@ export class Viewport extends Disposable implements IViewport {
private _getPixelsScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
if (ev.deltaY === 0 || ev.shiftKey) {
return 0;
}
@@ -238,7 +241,7 @@ export class Viewport extends Disposable implements IViewport {
*/
public getLinesScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
if (ev.deltaY === 0 || ev.shiftKey) {
return 0;
}
+3 -3
View File
@@ -121,7 +121,7 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
const line = bufferService.buffer.lines.get(startRow + (direction * i));
if (line && line.isWrapped) {
if (line?.isWrapped) {
wrappedRows++;
}
}
@@ -136,12 +136,12 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
let rowCount = 0;
let line = bufferService.buffer.lines.get(currentRow);
let lineWraps = line && line.isWrapped;
let lineWraps = line?.isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) {
rowCount++;
line = bufferService.buffer.lines.get(--currentRow);
lineWraps = line && line.isWrapped;
lineWraps = line?.isWrapped;
}
return rowCount;
+42 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm';
import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm';
import { ITerminal } from 'browser/Types';
import { Terminal as TerminalCore } from 'browser/Terminal';
import * as Strings from 'browser/LocalizableStrings';
@@ -12,16 +12,45 @@ import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import { AddonManager } from 'common/public/AddonManager';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
import { ITerminalOptions } from 'common/Types';
/**
* The set of options that only have an effect when set in the Terminal constructor.
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
export class Terminal implements ITerminalApi {
private _core: ITerminal;
private _addonManager: AddonManager;
private _parser: IParser | undefined;
private _buffer: BufferNamespaceApi | undefined;
private _publicOptions: ITerminalOptions;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
this._addonManager = new AddonManager();
this._publicOptions = {};
for (const propName in this._core.options) {
Object.defineProperty(this._publicOptions, propName, {
get: () => {
return this._core.options[propName];
},
set: (value: any) => {
this._checkReadonlyOptions(propName);
this._core.options[propName] = value;
}
});
}
}
private _checkReadonlyOptions(propName: string): void {
// Throw an error if any constructor only option is modified
// from terminal.options
// Modifications from anywhere else are allowed
if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {
throw new Error(`Option "${propName}" can only be set in the constructor`);
}
}
private _checkProposedApi(): void {
@@ -89,6 +118,14 @@ export class Terminal implements ITerminalApi {
wraparoundMode: m.wraparound
};
}
public get options(): ITerminalOptions {
return this._publicOptions;
}
public set options(options: ITerminalOptions) {
for (const propName in options) {
this._publicOptions[propName] = options[propName];
}
}
public blur(): void {
this._core.blur();
}
@@ -213,6 +250,7 @@ export class Terminal implements ITerminalApi {
public setOption(key: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void;
public setOption(key: any, value: any): void {
this._checkReadonlyOptions(key);
this._core.optionsService.setOption(key, value);
}
public refresh(start: number, end: number): void {
@@ -222,6 +260,9 @@ export class Terminal implements ITerminalApi {
public reset(): void {
this._core.reset();
}
public clearTextureAtlas(): void {
this._core.clearTextureAtlas();
}
public loadAddon(addon: ITerminalAddon): void {
return this._addonManager.loadAddon(this, addon);
}
+8 -9
View File
@@ -7,7 +7,7 @@ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
import { ICellData } from 'common/Types';
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData';
@@ -138,6 +138,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public abstract reset(): void;
public clearTextureAtlas(): void {
this._charAtlas?.clear();
}
/**
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
* @param x The column to start at.
@@ -258,7 +262,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
this._ctx.font = this._getFont(false, false);
this._ctx.textBaseline = 'ideographic';
this._ctx.textBaseline = TEXT_BASELINE;
this._clipRow(y);
// Draw custom characters if applicable
@@ -321,12 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._currentGlyphIdentifier.bold = !!cell.isBold();
this._currentGlyphIdentifier.dim = !!cell.isDim();
this._currentGlyphIdentifier.italic = !!cell.isItalic();
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
this._currentGlyphIdentifier,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
const atlasDidDraw = this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
if (!atlasDidDraw) {
this._drawUncachedChars(cell, x, y);
@@ -346,7 +345,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'ideographic';
this._ctx.textBaseline = TEXT_BASELINE;
if (cell.isInverse()) {
if (fgOverride) {
+7 -17
View File
@@ -55,7 +55,6 @@ export class CursorRenderLayer extends BaseRenderLayer {
'block': this._renderBlockCursor.bind(this),
'underline': this._renderUnderlineCursor.bind(this)
};
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public dispose(): void {
@@ -80,26 +79,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
public reset(): void {
this._clearCursor();
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = undefined;
this.onOptionsChanged();
}
this._cursorBlinkStateManager?.restartBlinkAnimation();
this.onOptionsChanged();
}
public onBlur(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause();
}
this._cursorBlinkStateManager?.pause();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onFocus(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume();
} else {
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
this._cursorBlinkStateManager?.resume();
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
}
public onOptionsChanged(): void {
@@ -119,9 +110,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onCursorMove(): void {
if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.restartBlinkAnimation();
}
this._cursorBlinkStateManager?.restartBlinkAnimation();
}
public onGridChanged(startRow: number, endRow: number): void {
@@ -313,6 +302,7 @@ class CursorBlinkStateManager {
// Clear any existing interval
if (this._blinkInterval) {
window.clearInterval(this._blinkInterval);
this._blinkInterval = undefined;
}
// Setup the initial timeout which will hide the cursor, this is done before
+6
View File
@@ -149,6 +149,12 @@ export class Renderer extends Disposable implements IRenderer {
}
}
public clearTextureAtlas(): void {
for (const layer of this._renderLayers) {
layer.clearTextureAtlas();
}
}
/**
* Recalculates the character and canvas dimensions.
*/
+6
View File
@@ -52,6 +52,7 @@ export interface IRenderer extends IDisposable {
onOptionsChanged(): void;
clear(): void;
renderRows(start: number, end: number): void;
clearTextureAtlas?(): void;
}
export interface IRenderLayer extends IDisposable {
@@ -100,4 +101,9 @@ export interface IRenderLayer extends IDisposable {
* Clear the state of the render layer.
*/
reset(): void;
/**
* Clears the texture atlas.
*/
clearTextureAtlas(): void;
}
@@ -28,6 +28,8 @@ export abstract class BaseCharAtlas implements IDisposable {
*/
private _doWarmUp(): void { }
public clear(): void { }
/**
* Called when we start drawing a new frame.
*
+1 -1
View File
@@ -16,7 +16,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
cursor: undefined,
cursorAccent: undefined,
selection: undefined,
ansi: colors.ansi
ansi: [...colors.ansi]
};
return {
devicePixelRatio: window.devicePixelRatio,
+6
View File
@@ -3,7 +3,13 @@
* @license MIT
*/
import { isFirefox } from 'common/Platform';
export const INVERTED_DEFAULT_COLOR = 257;
export const DIM_OPACITY = 0.5;
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox would
// result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly
// unaligned Powerline fonts (PR 3356#issuecomment-850928179).
export const TEXT_BASELINE: CanvasTextBaseline = isFirefox ? 'bottom' : 'ideographic';
export const CHAR_ATLAS_CELL_SPACING = 1;
+12 -2
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
@@ -119,6 +119,16 @@ export class DynamicCharAtlas extends BaseCharAtlas {
this._drawToCacheCount = 0;
}
public clear(): void {
if (this._cacheMap.size > 0) {
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
}
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
@@ -256,7 +266,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'ideographic';
this._tmpCtx.textBaseline = TEXT_BASELINE;
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
@@ -10,7 +10,7 @@ import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, FgFlags,
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
import { MockCoreService, MockOptionsService } from 'common/TestUtils.test';
import { css } from 'browser/Color';
import { MockCharacterJoinerService } from 'browser/TestUtils.test';
@@ -21,30 +21,36 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document, {
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any, new MockCharacterJoinerService(), new MockOptionsService({ drawBoldTextInBrightColors: true }));
rowFactory = new DomRendererRowFactory(
dom.window.document,
{
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
// dark:
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any,
new MockCharacterJoinerService(),
new MockOptionsService({ drawBoldTextInBrightColors: true }),
new MockCoreService()
);
lineData = createEmptyLineData(2);
});
@@ -7,7 +7,7 @@ import { IBufferLine } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { ICoreService, IOptionsService } from 'common/services/Services';
import { color, rgba } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
import { ICharacterJoinerService } from 'browser/services/Services';
@@ -31,7 +31,8 @@ export class DomRendererRowFactory {
private readonly _document: Document,
private _colors: IColorSet,
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
@IOptionsService private readonly _optionsService: IOptionsService
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreService private readonly _coreService: ICoreService
) {
}
@@ -110,7 +111,7 @@ export class DomRendererRowFactory {
}
}
if (isCursorRow && x === cursorX) {
if (!this._coreService.isCursorHidden && isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
if (cursorBlink) {
@@ -122,5 +122,13 @@ describe('SelectionModel', () => {
model.selectionEnd = [5, 2];
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
});
it('should not include a trailing EOL when the selection ends at the end of a line', () => {
model.selectionStart = [0, 0];
model.selectionStartLength = 80;
assert.deepEqual(model.finalSelectionEnd, [80, 0]);
model.selectionStart = [0, 0];
model.selectionStartLength = 160;
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
});
});
});
+4
View File
@@ -79,6 +79,10 @@ export class SelectionModel {
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
if (startPlusLength > this._bufferService.cols) {
// Ensure the trailing EOL isn't included when the selection ends on the right edge
if (startPlusLength % this._bufferService.cols === 0) {
return [this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols) - 1];
}
return [startPlusLength % this._bufferService.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._bufferService.cols)];
}
return [startPlusLength, this.selectionStart[1]];
+15 -6
View File
@@ -176,16 +176,25 @@ export class CharacterJoinerService implements ICharacterJoinerService {
// At this point we already know that there is at least one joiner so
// we can just pull its value and assign it directly rather than
// merging it into an empty array, which incurs unnecessary writes.
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
let allJoinedRanges: [number, number][] = [];
try {
allJoinedRanges = this._characterJoiners[0].handler(text);
} catch (error) {
console.error(error);
}
for (let i = 1; i < this._characterJoiners.length; i++) {
// We merge any overlapping ranges across the different joiners
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerService._mergeRanges(joinedRanges, joinerRanges[j]);
try {
const joinerRanges = this._characterJoiners[i].handler(text);
for (let j = 0; j < joinerRanges.length; j++) {
CharacterJoinerService._mergeRanges(allJoinedRanges, joinerRanges[j]);
}
} catch (error) {
console.error(error);
}
}
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
return joinedRanges;
this._stringRangesToCellRanges(allJoinedRanges, lineData, startCol);
return allJoinedRanges;
}
/**
+5
View File
@@ -168,6 +168,11 @@ export class RenderService extends Disposable implements IRenderService {
}
}
public clearTextureAtlas(): void {
this._renderer?.clearTextureAtlas?.();
this._fullRefresh();
}
public setColors(colors: IColorSet): void {
this._renderer.setColors(colors);
this._fullRefresh();
+2 -2
View File
@@ -220,7 +220,7 @@ export class SelectionService extends Disposable implements ISelectionService {
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = buffer.lines.get(i);
const lineText = buffer.translateBufferLineToString(i, true);
if (bufferLine && bufferLine.isWrapped) {
if (bufferLine?.isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
@@ -924,7 +924,7 @@ export class SelectionService extends Disposable implements ISelectionService {
if (followWrappedLinesBelow) {
if (start + length === this._bufferService.cols && bufferLine.getCodePoint(this._bufferService.cols - 1) !== 32 /* ' ' */) {
const nextBufferLine = buffer.lines.get(coords[1] + 1);
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /* ' ' */) {
if (nextBufferLine?.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /* ' ' */) {
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
if (nextLineWordPosition) {
length += nextLineWordPosition.length;
+1
View File
@@ -53,6 +53,7 @@ export interface IRenderService extends IDisposable {
dimensions: IRenderDimensions;
refreshRows(start: number, end: number): void;
clearTextureAtlas(): void;
resize(cols: number, rows: number): void;
changeOptions(): void;
setRenderer(renderer: IRenderer): void;
+9 -3
View File
@@ -22,12 +22,12 @@
*/
import { Disposable } from 'common/Lifecycle';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum } from 'common/services/Services';
import { IInstantiationService, IOptionsService, IBufferService, ILogService, ICharsetService, ICoreService, ICoreMouseService, IUnicodeService, IDirtyRowService, LogLevelEnum, ITerminalOptions } from 'common/services/Services';
import { InstantiationService } from 'common/services/InstantiationService';
import { LogService } from 'common/services/LogService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { OptionsService } from 'common/services/OptionsService';
import { ITerminalOptions, IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource } from 'common/Types';
import { IDisposable, IBufferLine, IAttributeData, ICoreTerminal, IKeyboardEvent, IScrollEvent, ScrollSource, ITerminalOptions as IPublicTerminalOptions } from 'common/Types';
import { CoreService } from 'common/services/CoreService';
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
import { CoreMouseService } from 'common/services/CoreMouseService';
@@ -86,9 +86,15 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
public get cols(): number { return this._bufferService.cols; }
public get rows(): number { return this._bufferService.rows; }
public get buffers(): IBufferSet { return this._bufferService.buffers; }
public get options(): ITerminalOptions { return this.optionsService.options; }
public set options(options: ITerminalOptions) {
for (const key in options) {
this.optionsService.options[key] = options[key];
}
}
constructor(
options: ITerminalOptions
options: Partial<ITerminalOptions>
) {
super();
+225 -90
View File
@@ -5,7 +5,7 @@
import { assert } from 'chai';
import { InputHandler } from 'common/InputHandler';
import { IBufferLine, IAttributeData, IAnsiColorChangeEvent } from 'common/Types';
import { IBufferLine, IAttributeData, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { Attributes, UnderlineStyle } from 'common/buffer/Constants';
@@ -17,7 +17,7 @@ import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
import { BufferService } from 'common/services/BufferService';
import { CoreService } from 'common/services/CoreService';
import { OscHandler } from 'common/parser/OscParser';
function getCursor(bufferService: IBufferService): number[] {
return [
@@ -41,7 +41,6 @@ class TestInputHandler extends InputHandler {
public get curAttrData(): IAttributeData { return (this as any)._curAttrData; }
public get windowTitleStack(): string[] { return this._windowTitleStack; }
public get iconNameStack(): string[] { return this._iconNameStack; }
public parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null { return this._parseAnsiColorChange(data); }
/**
* Promise based parse call to await the full resolve of given input data.
@@ -77,54 +76,54 @@ describe('InputHandler', () => {
optionsService.options.scrollback = 1;
bufferService.reset();
});
it('SL (scrollLeft)', () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[ @');
it('SL (scrollLeft)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[ @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']);
inputHandler.parseP('\x1b[0 @');
await inputHandler.parseP('\x1b[0 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']);
inputHandler.parseP('\x1b[2 @');
await inputHandler.parseP('\x1b[2 @');
assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[ A');
it('SR (scrollRight)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[ A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
inputHandler.parseP('\x1b[0 A');
await inputHandler.parseP('\x1b[0 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
inputHandler.parseP('\x1b[2 A');
await inputHandler.parseP('\x1b[2 A');
assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[\'}');
it('insertColumns (DECIC)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[1\'}');
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[1\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[2\'}');
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[2\'}');
assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', () => {
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[\'~');
it('deleteColumns (DECDC)', async () => {
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[1\'~');
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[1\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']);
bufferService.reset();
inputHandler.parseP('12345'.repeat(6));
inputHandler.parseP('\x1b[3;3H');
inputHandler.parseP('\x1b[2\'~');
await inputHandler.parseP('12345'.repeat(6));
await inputHandler.parseP('\x1b[3;3H');
await inputHandler.parseP('\x1b[2\'~');
assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']);
});
});
@@ -137,41 +136,41 @@ describe('InputHandler', () => {
bufferService.reset();
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', () => {
it('should not reverse outside of scroll margins', async () => {
// prepare buffer content
inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
await inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy');
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
assert.equal(bufferService.buffers.active.ydisp, 1);
assert.equal(bufferService.buffers.active.x, 5);
assert.equal(bufferService.buffers.active.y, 4);
inputHandler.parseP(ttyBS.repeat(100));
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
inputHandler.parseP('\x1b[?45h');
inputHandler.parseP('uvwxy');
await inputHandler.parseP('\x1b[?45h');
await inputHandler.parseP('uvwxy');
// set top/bottom to 1/3 (0-based)
inputHandler.parseP('\x1b[2;4r');
await inputHandler.parseP('\x1b[2;4r');
// place cursor below scroll bottom
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 4;
inputHandler.parseP(ttyBS.repeat(100));
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
inputHandler.parseP('uvwxy');
await inputHandler.parseP('uvwxy');
// place cursor within scroll margins
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 3;
inputHandler.parseP(ttyBS.repeat(100));
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
assert.equal(bufferService.buffers.active.x, 0);
assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop
inputHandler.parseP('fghijklmnopqrst');
await inputHandler.parseP('fghijklmnopqrst');
// place cursor above scroll top
bufferService.buffers.active.x = 5;
bufferService.buffers.active.y = 0;
inputHandler.parseP(ttyBS.repeat(100));
await inputHandler.parseP(ttyBS.repeat(100));
assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
@@ -386,6 +385,56 @@ describe('InputHandler', () => {
assert.equal(bufferService.buffer.lines.get(2)!.translateToString(false), Array(bufferService.cols + 1).join(' '));
});
it('eraseInLine reflow', async () => {
const bufferService = new MockBufferService(80, 30);
const inputHandler = new TestInputHandler(
bufferService,
new MockCharsetService(),
new MockCoreService(),
new MockDirtyRowService(),
new MockLogService(),
new MockOptionsService(),
new MockCoreMouseService(),
new MockUnicodeService()
);
const resetToBaseState = async (): Promise<void> => {
// reset and add a wrapped line
bufferService.buffer.y = 0;
bufferService.buffer.x = 0;
await inputHandler.parseP(Array(bufferService.cols + 1).join('a')); // line 0
await inputHandler.parseP(Array(bufferService.cols + 10).join('a')); // line 1 and 2
for (let i = 3; i < bufferService.rows; ++i) await inputHandler.parseP(Array(bufferService.cols + 1).join('a'));
// confirm precondition that line 2 is wrapped
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
};
// params[0] - erase from the cursor through the end of the row.
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([0]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
bufferService.buffer.y = 2;
bufferService.buffer.x = 0;
inputHandler.eraseInLine(Params.fromArray([0]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
// params[1] - erase from the beginning of the line through the cursor
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([1]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, true);
// params[2] - erase complete line
await resetToBaseState();
bufferService.buffer.y = 2;
bufferService.buffer.x = 40;
inputHandler.eraseInLine(Params.fromArray([2]));
assert.equal(bufferService.buffer.lines.get(2)!.isWrapped, false);
});
it('eraseInDisplay', async () => {
const bufferService = new MockBufferService(80, 7);
const inputHandler = new TestInputHandler(
@@ -1865,58 +1914,144 @@ describe('InputHandler', () => {
});
});
describe('OSC', () => {
it('4: should parse correct Ansi color change data', () => {
// this is testing a private method
const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3');
assert.isNotNull(event);
assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 });
it('4: query color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;?;123;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: 0 }, { type: ColorRequestType.REPORT, index: 123 }]]);
stack.length = 0;
});
it('4: set color events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single color query
await inputHandler.parseP('\x1b]4;0;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]4;123;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 123, color: [170, 187, 204] }]]);
stack.length = 0;
// multiple queries
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('4: should ignore invalid values', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]4;0;rgb:aa/bb/cc;45;rgb:1/22/333;123;#001122\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: 0, color: [170, 187, 204] }, { type: ColorRequestType.SET, index: 123, color: [0, 17, 34] }]]);
stack.length = 0;
});
it('104: restore events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]104;0\x07\x1b]104;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }], [{ type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// multiple in one command
await inputHandler.parseP('\x1b]104;0;43\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: 0 }, { type: ColorRequestType.RESTORE, index: 43 }]]);
stack.length = 0;
// full ANSI table restore
await inputHandler.parseP('\x1b]104\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE}]]);
});
it('4: should ignore incorrect Ansi color change data', () => {
// this is testing a private method
assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:a/b/c'));
assert.isNull(inputHandler.parseAnsiColorChange('17;rgb:#aabbcc'));
assert.isNull(inputHandler.parseAnsiColorChange('17;rgba:aa/bb/cc'));
assert.isNull(inputHandler.parseAnsiColorChange('rgb:aa/bb/cc'));
it('10: FG set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single foreground query --> color undefined
await inputHandler.parseP('\x1b]10;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.FOREGROUND }]]);
stack.length = 0;
// OSC with multiple values maps to OSC 10 & OSC 11 & OSC 12
await inputHandler.parseP('\x1b]10;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.FOREGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set foreground color events
await inputHandler.parseP('\x1b]10;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]10;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set FG, BG and cursor color at once
await inputHandler.parseP('\x1b]10;rgb:aa/bb/cc;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: ColorIndex.FOREGROUND, color: [170, 187, 204] }],
[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
it('4: should parse a list of Ansi color changes', () => {
// this is testing a private method
const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:00/11/22;255;rgb:01/ef/2d');
assert.isNotNull(event);
assert.equal(event!.colors.length, 3);
assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 });
assert.deepEqual(event!.colors[1], { colorIndex: 17, red: 0x00, green: 0x11, blue: 0x22 });
assert.deepEqual(event!.colors[2], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d });
it('110: restore FG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]110\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]]);
});
it('4: should ignore incorrect colors in a list of Ansi color changes', () => {
// this is testing a private method
const event = inputHandler.parseAnsiColorChange('19;rgb:a1/b2/c3;17;rgb:WR/ON/G;255;rgb:01/ef/2d');
assert.equal(event!.colors.length, 2);
assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 });
assert.deepEqual(event!.colors[1], { colorIndex: 255, red: 0x01, green: 0xef, blue: 0x2d });
it('11: BG set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single background query --> color undefined
await inputHandler.parseP('\x1b]11;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }]]);
stack.length = 0;
// OSC 11 with multiple values creates only BG and cursor event
await inputHandler.parseP('\x1b]11;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.BACKGROUND }], [{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set background color events
await inputHandler.parseP('\x1b]11;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]11;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [170, 187, 204] }]]);
stack.length = 0;
// set BG and cursor color at once
await inputHandler.parseP('\x1b]11;#001122;rgb:12/34/56\x07');
assert.deepEqual(stack, [
[{ type: ColorRequestType.SET, index: ColorIndex.BACKGROUND, color: [0, 17, 34] }],
[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [18, 52, 86] }]
]);
});
it('4: should be case insensitive when parsing Ansi color changes', () => {
// this is testing a private method
const event = inputHandler.parseAnsiColorChange('19;rGb:A1/b2/C3');
assert.equal(event!.colors.length, 1);
assert.deepEqual(event!.colors[0], { colorIndex: 19, red: 0xa1, green: 0xb2, blue: 0xc3 });
it('111: restore BG color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]111\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]]);
});
it('4: should fire event on Ansi color change', async () => {
return new Promise(async r => {
inputHandler.onAnsiColorChange(e => {
assert.isNotNull(e);
assert.isNotNull(e!.colors);
assert.deepEqual(e!.colors[0], { colorIndex: 17, red: 0x1a, green: 0x2b, blue: 0x3c });
assert.deepEqual(e!.colors[1], { colorIndex: 12, red: 0x11, green: 0x22, blue: 0x33 });
r();
});
await inputHandler.parseP('\x1b]4;17;rgb:1a/2b/3c;12;rgb:11/22/33\x1b\\');
});
it('12: cursor color set & query events', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
// single cursor query --> color undefined
await inputHandler.parseP('\x1b]12;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// OSC 12 with multiple values creates only cursor event
await inputHandler.parseP('\x1b]12;?;?;?;?\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.REPORT, index: ColorIndex.CURSOR }]]);
stack.length = 0;
// set cursor color events
await inputHandler.parseP('\x1b]12;rgb:01/02/03\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [1, 2, 3] }]]);
stack.length = 0;
await inputHandler.parseP('\x1b]12;#aabbcc\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.SET, index: ColorIndex.CURSOR, color: [170, 187, 204] }]]);
});
it('112: restore cursor color', async () => {
const stack: IColorEvent[] = [];
inputHandler.onColor(ev => stack.push(ev));
await inputHandler.parseP('\x1b]112\x07');
assert.deepEqual(stack, [[{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]]);
});
});
+187 -49
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IColorEvent, IParseStack, ColorIndex, ColorRequestType } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -21,11 +21,12 @@ import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowSe
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
import { IBuffer } from 'common/buffer/Types';
import { parseColor } from 'common/input/XParseColor';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL: {[key: string]: number} = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
const GLEVEL: { [key: string]: number } = { '(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2 };
/**
* VT commands done by the parser - FIXME: move this to the parser?
@@ -167,7 +168,7 @@ class DECRQSS implements IDcsHandler {
break;
case 'r': // DECSTBM
const pt = '' + (this._bufferService.buffer.scrollTop + 1) +
';' + (this._bufferService.buffer.scrollBottom + 1) + 'r';
';' + (this._bufferService.buffer.scrollBottom + 1) + 'r';
this._coreService.triggerDataEvent(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
break;
case 'm': // SGR
@@ -175,7 +176,7 @@ class DECRQSS implements IDcsHandler {
this._coreService.triggerDataEvent(`${C0.ESC}P1$r0m${C0.ESC}\\`);
break;
case ' q': // DECSCUSR
const STYLES: {[key: string]: number} = { 'block': 2, 'underline': 4, 'bar': 6 };
const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };
let style = STYLES[this._optionsService.options.cursorStyle];
style -= this._optionsService.options.cursorBlink ? 1 : 0;
this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
@@ -243,6 +244,8 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onRequestRefreshRows(): IEvent<number, number> { return this._onRequestRefreshRows.event; }
private _onRequestReset = new EventEmitter<void>();
public get onRequestReset(): IEvent<void> { return this._onRequestReset.event; }
private _onRequestSendFocus = new EventEmitter<void>();
public get onRequestSendFocus(): IEvent<void> { return this._onRequestSendFocus.event; }
private _onRequestSyncScrollBar = new EventEmitter<void>();
public get onRequestSyncScrollBar(): IEvent<void> { return this._onRequestSyncScrollBar.event; }
private _onRequestWindowsOptionsReport = new EventEmitter<WindowsOptionsReportType>();
@@ -260,8 +263,8 @@ export class InputHandler extends Disposable implements IInputHandler {
public get onScroll(): IEvent<number> { return this._onScroll.event; }
private _onTitleChange = new EventEmitter<string>();
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
private _onAnsiColorChange = new EventEmitter<IAnsiColorChangeEvent>();
public get onAnsiColorChange(): IEvent<IAnsiColorChangeEvent> { return this._onAnsiColorChange.event; }
private _onColor = new EventEmitter<IColorEvent>();
public get onColor(): IEvent<IColorEvent> { return this._onColor.event; }
private _parseStack: IParseStack = {
paused: false,
@@ -396,13 +399,16 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.registerOscHandler(2, new OscHandler(data => this.setTitle(data)));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
this._parser.registerOscHandler(4, new OscHandler(data => this.setAnsiColor(data)));
this._parser.registerOscHandler(4, new OscHandler(data => this.setOrReportIndexedColor(data)));
// 5 - Change Special Color Number
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 10 - Change VT100 text foreground color to Pt.
this._parser.registerOscHandler(10, new OscHandler(data => this.setOrReportFgColor(data)));
// 11 - Change VT100 text background color to Pt.
this._parser.registerOscHandler(11, new OscHandler(data => this.setOrReportBgColor(data)));
// 12 - Change text cursor color to Pt.
this._parser.registerOscHandler(12, new OscHandler(data => this.setOrReportCursorColor(data)));
// 13 - Change mouse foreground color to Pt.
// 14 - Change mouse background color to Pt.
// 15 - Change Tektronix foreground color to Pt.
@@ -415,11 +421,15 @@ export class InputHandler extends Disposable implements IInputHandler {
// 51 - reserved for Emacs shell.
// 52 - Manipulate Selection Data.
// 104 ; c - Reset Color Number c.
this._parser.registerOscHandler(104, new OscHandler(data => this.restoreIndexedColor(data)));
// 105 ; c - Reset Special Color Number c.
// 106 ; c; f - Enable/disable Special Color Number c.
// 110 - Reset VT100 text foreground color.
this._parser.registerOscHandler(110, new OscHandler(data => this.restoreFgColor(data)));
// 111 - Reset VT100 text background color.
this._parser.registerOscHandler(111, new OscHandler(data => this.restoreBgColor(data)));
// 112 - Reset text cursor color.
this._parser.registerOscHandler(112, new OscHandler(data => this.restoreCursorColor(data)));
// 113 - Reset mouse foreground color.
// 114 - Reset mouse background color.
// 115 - Reset Tektronix foreground color.
@@ -534,7 +544,13 @@ export class InputHandler extends Disposable implements IInputHandler {
}
}
this._logService.debug('parsing data', data);
// Log debug data, the log level gate is to prevent extra work in this hot path
if (this._logService.logLevel <= LogLevelEnum.DEBUG) {
this._logService.debug(`parsing data${typeof data === 'string' ? ` "${data}"` : ''}`, typeof data === 'string'
? data.split('').map(e => e.charCodeAt(0))
: data
);
}
// resize input buffer if needed
if (this._parseBuffer.length < data.length) {
@@ -853,10 +869,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* - any cursor movement sequence keeps working as expected
*/
if (this._activeBuffer.x === 0
&& this._activeBuffer.y > this._activeBuffer.scrollTop
&& this._activeBuffer.y <= this._activeBuffer.scrollBottom
&& this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped)
{
&& this._activeBuffer.y > this._activeBuffer.scrollTop
&& this._activeBuffer.y <= this._activeBuffer.scrollBottom
&& this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
this._activeBuffer.y--;
this._activeBuffer.x = this._bufferService.cols - 1;
@@ -1193,6 +1208,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* @param y row index
* @param start first cell index to be erased
* @param end end - 1 is last erased cell
* @param cleanWrap clear the isWrapped flag
*/
private _eraseInBufferLine(y: number, start: number, end: number, clearWrap: boolean = false): void {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
@@ -1318,13 +1334,13 @@ export class InputHandler extends Disposable implements IInputHandler {
this._restrictCursor(this._bufferService.cols);
switch (params.params[0]) {
case 0:
this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols);
this._eraseInBufferLine(this._activeBuffer.y, this._activeBuffer.x, this._bufferService.cols, this._activeBuffer.x === 0);
break;
case 1:
this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1);
this._eraseInBufferLine(this._activeBuffer.y, 0, this._activeBuffer.x + 1, false);
break;
case 2:
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols);
this._eraseInBufferLine(this._activeBuffer.y, 0, this._bufferService.cols, true);
break;
}
this._dirtyRowService.markDirty(this._activeBuffer.y);
@@ -1956,6 +1972,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// focusin: ^[[I
// focusout: ^[[O
this._coreService.decPrivateModes.sendFocus = true;
this._onRequestSendFocus.fire();
break;
case 1005: // utf8 ext mode mouse - removed in #2507
this._logService.debug('DECSET 1005 not supported (see #2507)');
@@ -1974,7 +1991,7 @@ export class InputHandler extends Disposable implements IInputHandler {
break;
case 1049: // alt screen buffer cursor
this.saveCursor();
// FALL-THROUGH
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._bufferService.buffers.activateAltBuffer(this._eraseAttrData());
@@ -2194,7 +2211,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this.restoreCursor();
break;
case 1049: // alt screen buffer cursor
// FALL-THROUGH
// FALL-THROUGH
case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first
// Ensure the selection manager has the correct buffer
@@ -2261,7 +2278,7 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// exit early if can decide color mode with semicolons
if ((accu[1] === 5 && advance + cSpace >= 2)
|| (accu[1] === 2 && advance + cSpace >= 5)) {
|| (accu[1] === 2 && advance + cSpace >= 5)) {
break;
}
// offset colorSpace slot for semicolon mode
@@ -2680,7 +2697,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const top = params.params[0] || 1;
let bottom: number;
if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {
if (params.length < 2 || (bottom = params.params[1]) > this._bufferService.rows || bottom === 0) {
bottom = this._bufferService.rows;
}
@@ -2837,46 +2854,167 @@ export class InputHandler extends Disposable implements IInputHandler {
return true;
}
protected _parseAnsiColorChange(data: string): IAnsiColorChangeEvent | null {
const result: IAnsiColorChangeEvent = { colors: [] };
// example data: 5;rgb:aa/bb/cc
const regex = /(\d+);rgb:([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})/gi;
let match;
while ((match = regex.exec(data)) !== null) {
result.colors.push({
colorIndex: parseInt(match[1]),
red: parseInt(match[2], 16),
green: parseInt(match[3], 16),
blue: parseInt(match[4], 16)
});
}
if (result.colors.length === 0) {
return null;
}
return result;
}
/**
* OSC 4; <num> ; <text> ST (set ANSI color <num> to <text>)
*
* @vt: #Y OSC 4 "Set ANSI color" "OSC 4 ; c ; spec BEL" "Change color number `c` to the color specified by `spec`."
* `c` is the color index between 0 and 255. `spec` color format is 'rgb:hh/hh/hh' where `h` are hexadecimal digits.
* There may be multipe c ; spec elements present in the same instruction, e.g. 1;rgb:10/20/30;2;rgb:a0/b0/c0.
* `c` is the color index between 0 and 255. The color format of `spec` is derived from `XParseColor` (see OSC 10 for supported formats).
* There may be multipe `c ; spec` pairs present in the same instruction.
* If `spec` contains `?` the terminal returns a sequence with the currently set color.
*/
public setAnsiColor(data: string): boolean {
const event = this._parseAnsiColorChange(data);
if (event) {
this._onAnsiColorChange.fire(event);
public setOrReportIndexedColor(data: string): boolean {
const event: IColorEvent = [];
const slots = data.split(';');
while (slots.length > 1) {
const idx = slots.shift() as string;
const spec = slots.shift() as string;
if (/^\d+$/.exec(idx)) {
const index = parseInt(idx);
if (0 <= index && index < 256) {
if (spec === '?') {
event.push({ type: ColorRequestType.REPORT, index });
} else {
const color = parseColor(spec);
if (color) {
event.push({ type: ColorRequestType.SET, index, color });
}
}
}
}
}
else {
this._logService.warn(`Expected format <num>;rgb:<rr>/<gg>/<bb> but got data: ${data}`);
if (event.length) {
this._onColor.fire(event);
}
return true;
}
// special colors - OSC 10 | 11 | 12
private _specialColors = [ColorIndex.FOREGROUND, ColorIndex.BACKGROUND, ColorIndex.CURSOR];
/**
* Apply colors requests for special colors in OSC 10 | 11 | 12.
* Since these commands are stacking from multiple parameters,
* we handle them in a loop with an entry offset to `_specialColors`.
*/
private _setOrReportSpecialColor(data: string, offset: number): boolean {
const slots = data.split(';');
for (let i = 0; i < slots.length; ++i, ++offset) {
if (offset >= this._specialColors.length) break;
if (slots[i] === '?') {
this._onColor.fire([{ type: ColorRequestType.REPORT, index: this._specialColors[offset] }]);
} else {
const color = parseColor(slots[i]);
if (color) {
this._onColor.fire([{ type: ColorRequestType.SET, index: this._specialColors[offset], color }]);
}
}
}
return true;
}
/**
* OSC 10 ; <xcolor name>|<?> ST - set or query default foreground color
*
* @vt: #Y OSC 10 "Set or query default foreground color" "OSC 10 ; Pt BEL" "Set or query default foreground color."
* To set the color, the following color specification formats are supported:
* - `rgb:<red>/<green>/<blue>` for `<red>, <green>, <blue>` in `h | hh | hhh | hhhh`, where
* `h` is a single hexadecimal digit (case insignificant). The different widths scale
* from 4 bit (`h`) to 16 bit (`hhhh`) and get converted to 8 bit (`hh`).
* - `#RGB` - 4 bits per channel, expanded to `#R0G0B0`
* - `#RRGGBB` - 8 bits per channel
* - `#RRRGGGBBB` - 12 bits per channel, truncated to `#RRGGBB`
* - `#RRRRGGGGBBBB` - 16 bits per channel, truncated to `#RRGGBB`
*
* **Note:** X11 named colors are currently unsupported.
*
* If `Pt` contains `?` instead of a color specification, the terminal
* returns a sequence with the current default foreground color
* (use that sequence to restore the color after changes).
*
* **Note:** Other than xterm, xterm.js does not support OSC 12 - 19.
* Therefore stacking multiple `Pt` separated by `;` only works for the first two entries.
*/
public setOrReportFgColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 0);
}
/**
* OSC 11 ; <xcolor name>|<?> ST - set or query default background color
*
* @vt: #Y OSC 11 "Set or query default background color" "OSC 11 ; Pt BEL" "Same as OSC 10, but for default background."
*/
public setOrReportBgColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 1);
}
/**
* OSC 12 ; <xcolor name>|<?> ST - set or query default cursor color
*
* @vt: #Y OSC 12 "Set or query default cursor color" "OSC 12 ; Pt BEL" "Same as OSC 10, but for default cursor color."
*/
public setOrReportCursorColor(data: string): boolean {
return this._setOrReportSpecialColor(data, 2);
}
/**
* OSC 104 ; <num> ST - restore ANSI color <num>
*
* @vt: #Y OSC 104 "Reset ANSI color" "OSC 104 ; c BEL" "Reset color number `c` to themed color."
* `c` is the color index between 0 and 255. This function restores the default color for `c` as
* specified by the loaded theme. Any number of `c` parameters may be given.
* If no parameters are given, the entire indexed color table will be reset.
*/
public restoreIndexedColor(data: string): boolean {
if (!data) {
this._onColor.fire([{ type: ColorRequestType.RESTORE }]);
return true;
}
const event: IColorEvent = [];
const slots = data.split(';');
for (let i = 0; i < slots.length; ++i) {
if (/^\d+$/.exec(slots[i])) {
const index = parseInt(slots[i]);
if (0 <= index && index < 256) {
event.push({ type: ColorRequestType.RESTORE, index });
}
}
}
if (event.length) {
this._onColor.fire(event);
}
return true;
}
/**
* OSC 110 ST - restore default foreground color
*
* @vt: #Y OSC 110 "Restore default foreground color" "OSC 110 BEL" "Restore default foreground to themed color."
*/
public restoreFgColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.FOREGROUND }]);
return true;
}
/**
* OSC 111 ST - restore default background color
*
* @vt: #Y OSC 111 "Restore default background color" "OSC 111 BEL" "Restore default background to themed color."
*/
public restoreBgColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.BACKGROUND }]);
return true;
}
/**
* OSC 112 ST - restore default cursor color
*
* @vt: #Y OSC 112 "Restore default cursor color" "OSC 112 BEL" "Restore default cursor to themed color."
*/
public restoreCursorColor(data: string): boolean {
this._onColor.fire([{ type: ColorRequestType.RESTORE, index: ColorIndex.CURSOR }]);
return true;
}
/**
* ESC E
* C1.NEL
+8 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
@@ -122,13 +122,18 @@ export class MockOptionsService implements IOptionsService {
public serviceBrand: any;
public options: ITerminalOptions = clone(DEFAULT_OPTIONS);
public onOptionChange: IEvent<string> = new EventEmitter<string>().event;
constructor(testOptions?: IPartialTerminalOptions) {
constructor(testOptions?: Partial<ITerminalOptions>) {
if (testOptions) {
for (const key of Object.keys(testOptions)) {
this.options[key] = (testOptions as any)[key];
this.options[key] = testOptions[key];
}
}
}
public setOptions(options: ITerminalOptions): void {
for (const key of Object.keys(options)) {
this.options[key] = options[key];
}
}
public setOption<T>(key: string, value: T): void {
throw new Error('Method not implemented.');
}
+96 -72
View File
@@ -16,6 +16,7 @@ export interface ICoreTerminal {
optionsService: IOptionsService;
unicodeService: IUnicodeService;
buffers: IBufferSet;
options: ITerminalOptions;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
@@ -45,6 +46,7 @@ export interface IKeyboardEvent {
ctrlKey: boolean;
shiftKey: boolean;
metaKey: boolean;
/** @deprecated See KeyboardEvent.keyCode */
keyCode: number;
key: string;
type: string;
@@ -347,19 +349,32 @@ export interface IWindowOptions {
setWinLines?: boolean;
}
export interface IAnsiColorChangeEventColor {
colorIndex: number;
red: number;
green: number;
blue: number;
// color events from common, used for OSC 4/10/11/12 and 104/110/111/112
export const enum ColorRequestType {
REPORT = 0,
SET = 1,
RESTORE = 2
}
export const enum ColorIndex {
FOREGROUND = 256,
BACKGROUND = 257,
CURSOR = 258
}
export interface IColorReportRequest {
type: ColorRequestType.REPORT;
index: ColorIndex;
}
export interface IColorSetRequest {
type: ColorRequestType.SET;
index: ColorIndex;
color: IColorRGB;
}
export interface IColorRestoreRequest {
type: ColorRequestType.RESTORE;
index?: ColorIndex;
}
export type IColorEvent = (IColorReportRequest | IColorSetRequest | IColorRestoreRequest)[];
/**
* Event fired for OSC 4 command - to change ANSI color based on its index.
*/
export interface IAnsiColorChangeEvent {
colors: IAnsiColorChangeEventColor[];
}
/**
* Calls the parser and handles actions generated by the parser.
@@ -374,80 +389,89 @@ export interface IInputHandler {
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** C0 BEL */ bell(): boolean;
/** C0 LF */ lineFeed(): boolean;
/** C0 CR */ carriageReturn(): boolean;
/** C0 BS */ backspace(): boolean;
/** C0 HT */ tab(): boolean;
/** C0 SO */ shiftOut(): boolean;
/** C0 SI */ shiftIn(): boolean;
/** CSI @ */ insertChars(params: IParams): boolean;
/** CSI SP @ */ scrollLeft(params: IParams): boolean;
/** CSI A */ cursorUp(params: IParams): boolean;
/** CSI SP A */ scrollRight(params: IParams): boolean;
/** CSI B */ cursorDown(params: IParams): boolean;
/** CSI C */ cursorForward(params: IParams): boolean;
/** CSI D */ cursorBackward(params: IParams): boolean;
/** CSI E */ cursorNextLine(params: IParams): boolean;
/** CSI F */ cursorPrecedingLine(params: IParams): boolean;
/** CSI G */ cursorCharAbsolute(params: IParams): boolean;
/** CSI H */ cursorPosition(params: IParams): boolean;
/** CSI I */ cursorForwardTab(params: IParams): boolean;
/** CSI J */ eraseInDisplay(params: IParams): boolean;
/** CSI K */ eraseInLine(params: IParams): boolean;
/** CSI L */ insertLines(params: IParams): boolean;
/** CSI M */ deleteLines(params: IParams): boolean;
/** CSI P */ deleteChars(params: IParams): boolean;
/** CSI S */ scrollUp(params: IParams): boolean;
/** CSI T */ scrollDown(params: IParams, collect?: string): boolean;
/** CSI X */ eraseChars(params: IParams): boolean;
/** CSI Z */ cursorBackwardTab(params: IParams): boolean;
/** CSI ` */ charPosAbsolute(params: IParams): boolean;
/** CSI a */ hPositionRelative(params: IParams): boolean;
/** CSI b */ repeatPrecedingCharacter(params: IParams): boolean;
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): boolean;
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): boolean;
/** CSI d */ linePosAbsolute(params: IParams): boolean;
/** CSI e */ vPositionRelative(params: IParams): boolean;
/** CSI f */ hVPosition(params: IParams): boolean;
/** CSI g */ tabClear(params: IParams): boolean;
/** CSI h */ setMode(params: IParams, collect?: string): boolean;
/** CSI l */ resetMode(params: IParams, collect?: string): boolean;
/** CSI m */ charAttributes(params: IParams): boolean;
/** CSI n */ deviceStatus(params: IParams, collect?: string): boolean;
/** CSI p */ softReset(params: IParams, collect?: string): boolean;
/** CSI q */ setCursorStyle(params: IParams, collect?: string): boolean;
/** CSI r */ setScrollRegion(params: IParams, collect?: string): boolean;
/** CSI s */ saveCursor(params: IParams): boolean;
/** CSI u */ restoreCursor(params: IParams): boolean;
/** CSI ' } */ insertColumns(params: IParams): boolean;
/** CSI ' ~ */ deleteColumns(params: IParams): boolean;
/** CSI @ */ insertChars(params: IParams): void;
/** CSI SP @ */ scrollLeft(params: IParams): void;
/** CSI A */ cursorUp(params: IParams): void;
/** CSI SP A */ scrollRight(params: IParams): void;
/** CSI B */ cursorDown(params: IParams): void;
/** CSI C */ cursorForward(params: IParams): void;
/** CSI D */ cursorBackward(params: IParams): void;
/** CSI E */ cursorNextLine(params: IParams): void;
/** CSI F */ cursorPrecedingLine(params: IParams): void;
/** CSI G */ cursorCharAbsolute(params: IParams): void;
/** CSI H */ cursorPosition(params: IParams): void;
/** CSI I */ cursorForwardTab(params: IParams): void;
/** CSI J */ eraseInDisplay(params: IParams): void;
/** CSI K */ eraseInLine(params: IParams): void;
/** CSI L */ insertLines(params: IParams): void;
/** CSI M */ deleteLines(params: IParams): void;
/** CSI P */ deleteChars(params: IParams): void;
/** CSI S */ scrollUp(params: IParams): void;
/** CSI T */ scrollDown(params: IParams, collect?: string): void;
/** CSI X */ eraseChars(params: IParams): void;
/** CSI Z */ cursorBackwardTab(params: IParams): void;
/** CSI ` */ charPosAbsolute(params: IParams): void;
/** CSI a */ hPositionRelative(params: IParams): void;
/** CSI b */ repeatPrecedingCharacter(params: IParams): void;
/** CSI c */ sendDeviceAttributesPrimary(params: IParams): void;
/** CSI > c */ sendDeviceAttributesSecondary(params: IParams): void;
/** CSI d */ linePosAbsolute(params: IParams): void;
/** CSI e */ vPositionRelative(params: IParams): void;
/** CSI f */ hVPosition(params: IParams): void;
/** CSI g */ tabClear(params: IParams): void;
/** CSI h */ setMode(params: IParams, collect?: string): void;
/** CSI l */ resetMode(params: IParams, collect?: string): void;
/** CSI m */ charAttributes(params: IParams): void;
/** CSI n */ deviceStatus(params: IParams, collect?: string): void;
/** CSI p */ softReset(params: IParams, collect?: string): void;
/** CSI q */ setCursorStyle(params: IParams, collect?: string): void;
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
/** CSI s */ saveCursor(params: IParams): void;
/** CSI u */ restoreCursor(params: IParams): void;
/** CSI ' } */ insertColumns(params: IParams): void;
/** CSI ' ~ */ deleteColumns(params: IParams): void;
/** OSC 0
OSC 2 */ setTitle(data: string): void;
/** OSC 4 */ setAnsiColor(data: string): void;
/** ESC E */ nextLine(): void;
/** ESC = */ keypadApplicationMode(): void;
/** ESC > */ keypadNumericMode(): void;
OSC 2 */ setTitle(data: string): boolean;
/** OSC 4 */ setOrReportIndexedColor(data: string): boolean;
/** OSC 10 */ setOrReportFgColor(data: string): boolean;
/** OSC 11 */ setOrReportBgColor(data: string): boolean;
/** OSC 12 */ setOrReportCursorColor(data: string): boolean;
/** OSC 104 */ restoreIndexedColor(data: string): boolean;
/** OSC 110 */ restoreFgColor(data: string): boolean;
/** OSC 111 */ restoreBgColor(data: string): boolean;
/** OSC 112 */ restoreCursorColor(data: string): boolean;
/** ESC E */ nextLine(): boolean;
/** ESC = */ keypadApplicationMode(): boolean;
/** ESC > */ keypadNumericMode(): boolean;
/** ESC % G
ESC % @ */ selectDefaultCharset(): void;
ESC % @ */ selectDefaultCharset(): boolean;
/** ESC ( C
ESC ) C
ESC * C
ESC + C
ESC - C
ESC . C
ESC / C */ selectCharset(collectAndFlag: string): void;
/** ESC D */ index(): void;
/** ESC H */ tabSet(): void;
/** ESC M */ reverseIndex(): void;
/** ESC c */ fullReset(): void;
ESC / C */ selectCharset(collectAndFlag: string): boolean;
/** ESC D */ index(): boolean;
/** ESC H */ tabSet(): boolean;
/** ESC M */ reverseIndex(): boolean;
/** ESC c */ fullReset(): boolean;
/** ESC n
ESC o
ESC |
ESC }
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
ESC ~ */ setgLevel(level: number): boolean;
/** ESC # 8 */ screenAlignmentPattern(): boolean;
}
interface IParseStack {
+1 -1
View File
@@ -349,7 +349,7 @@ export function evaluateKeyboardEvent(
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
const key = keyMapping?.[!ev.shiftKey ? 0 : 1];
if (key) {
result.key = C0.ESC + key;
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
+1 -1
View File
@@ -162,7 +162,7 @@ it('wcwidth should match all values from the old implementation', function(): vo
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return (num: number): number => {
num = num | 0; // get asm.js like optimization under V8
num |= 0; // get asm.js like optimization under V8
if (num < 32) {
return control | 0;
}
+93
View File
@@ -0,0 +1,93 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { parseColor, toRgbString } from 'common/input/XParseColor';
describe('XParseColor', () => {
describe('parseColor', () => {
it('rgb:<r>/<g>/<b> scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(parseColor('rgb:0/0/0'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:f/f/f'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:1/2/3'), [17, 34, 51]);
// 8 bit
assert.deepEqual(parseColor('rgb:00/00/00'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:ff/ff/ff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:11/22/33'), [17, 34, 51]);
// 12 bit
assert.deepEqual(parseColor('rgb:000/000/000'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:fff/fff/fff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:111/222/333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(parseColor('rgb:0000/0000/0000'), [0, 0, 0]);
assert.deepEqual(parseColor('rgb:ffff/ffff/ffff'), [255, 255, 255]);
assert.deepEqual(parseColor('rgb:1111/2222/3333'), [17, 34, 51]);
});
it('#RGB scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.deepEqual(parseColor('#000'), [0, 0, 0]);
assert.deepEqual(parseColor('#fff'), [240, 240, 240]);
assert.deepEqual(parseColor('#123'), [16, 32, 48]);
// 8 bit
assert.deepEqual(parseColor('#000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#ffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#112233'), [17, 34, 51]);
// 12 bit
assert.deepEqual(parseColor('#000000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#fffffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#111222333'), [17, 34, 51]);
// 16 bit
assert.deepEqual(parseColor('#000000000000'), [0, 0, 0]);
assert.deepEqual(parseColor('#ffffffffffff'), [255, 255, 255]);
assert.deepEqual(parseColor('#111122223333'), [17, 34, 51]);
});
it('supports upper case', () => {
assert.deepEqual(parseColor('RGB:0/A/F'), [0, 170, 255]);
assert.deepEqual(parseColor('#FFF'), [240, 240, 240]);
});
it('does not parse illegal combinations', () => {
// shifting bit width
assert.equal(parseColor('rgb:0/11/222'), undefined);
// unsupported scheme
assert.equal(parseColor('rgbi:00/11/22'), undefined);
// broken # specifier
assert.equal(parseColor('#aabbbcc'), undefined);
// out of range
assert.equal(parseColor('#aabbgg'), undefined);
assert.equal(parseColor('rgb:aa/bb/gg'), undefined);
});
});
describe('toXColorRgb', () => {
it('rgb:<r>/<g>/<b> scheme in 4/8/12/16 bit', () => {
// 4 bit
assert.equal(toRgbString(parseColor('rgb:0/0/0')!, 4), 'rgb:0/0/0');
assert.equal(toRgbString(parseColor('rgb:f/f/f')!, 4), 'rgb:f/f/f');
assert.equal(toRgbString(parseColor('rgb:1/2/3')!, 4), 'rgb:1/2/3');
// 8 bit
assert.equal(toRgbString(parseColor('rgb:00/00/00')!, 8), 'rgb:00/00/00');
assert.equal(toRgbString(parseColor('rgb:ff/ff/ff')!, 8), 'rgb:ff/ff/ff');
assert.equal(toRgbString(parseColor('rgb:11/22/33')!, 8), 'rgb:11/22/33');
// 12 bit
assert.equal(toRgbString(parseColor('rgb:000/000/000')!, 12), 'rgb:000/000/000');
assert.equal(toRgbString(parseColor('rgb:fff/fff/fff')!, 12), 'rgb:fff/fff/fff');
assert.equal(toRgbString(parseColor('rgb:111/222/333')!, 12), 'rgb:111/222/333');
// 16 bit
assert.equal(toRgbString(parseColor('rgb:0000/0000/0000')!, 16), 'rgb:0000/0000/0000');
assert.equal(toRgbString(parseColor('rgb:ffff/ffff/ffff')!, 16), 'rgb:ffff/ffff/ffff');
assert.equal(toRgbString(parseColor('rgb:1111/2222/3333')!, 16), 'rgb:1111/2222/3333');
});
it('defaults to 16 bit output', () => {
assert.equal(toRgbString(parseColor('rgb:1/2/3')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:11/22/33')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:111/222/333')!), 'rgb:1111/2222/3333');
assert.equal(toRgbString(parseColor('rgb:123/123/123')!), 'rgb:1212/1212/1212');
});
it('reduces colors to 8 bit resolution', () => {
assert.equal(toRgbString(parseColor('rgb:123/123/123')!, 12), 'rgb:121/121/121');
assert.equal(toRgbString(parseColor('rgb:1234/1234/1234')!, 16), 'rgb:1212/1212/1212');
});
});
});
+80
View File
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
* @license MIT
*/
// 'rgb:' rule - matching: r/g/b | rr/gg/bb | rrr/ggg/bbb | rrrr/gggg/bbbb (hex digits)
const RGB_REX = /^([\da-f]{1})\/([\da-f]{1})\/([\da-f]{1})$|^([\da-f]{2})\/([\da-f]{2})\/([\da-f]{2})$|^([\da-f]{3})\/([\da-f]{3})\/([\da-f]{3})$|^([\da-f]{4})\/([\da-f]{4})\/([\da-f]{4})$/;
// '#...' rule - matching any hex digits
const HASH_REX = /^[\da-f]+$/;
/**
* Parse color spec to RGB values (8 bit per channel).
* See `man xparsecolor` for details about certain format specifications.
*
* Supported formats:
* - rgb:<red>/<green>/<blue> with <red>, <green>, <blue> in h | hh | hhh | hhhh
* - #RGB, #RRGGBB, #RRRGGGBBB, #RRRRGGGGBBBB
*
* All other formats like rgbi: or device-independent string specifications
* with float numbering are not supported.
*/
export function parseColor(data: string): [number, number, number] | undefined {
if (!data) return;
// also handle uppercases
let low = data.toLowerCase();
if (low.indexOf('rgb:') === 0) {
// 'rgb:' specifier
low = low.slice(4);
const m = RGB_REX.exec(low);
if (m) {
const base = m[1] ? 15 : m[4] ? 255 : m[7] ? 4095 : 65535;
return [
Math.round(parseInt(m[1] || m[4] || m[7] || m[10], 16) / base * 255),
Math.round(parseInt(m[2] || m[5] || m[8] || m[11], 16) / base * 255),
Math.round(parseInt(m[3] || m[6] || m[9] || m[12], 16) / base * 255)
];
}
} else if (low.indexOf('#') === 0) {
// '#' specifier
low = low.slice(1);
if (HASH_REX.exec(low) && [3, 6, 9, 12].includes(low.length)) {
const adv = low.length / 3;
const result: [number, number, number] = [0, 0, 0];
for (let i = 0; i < 3; ++i) {
const c = parseInt(low.slice(adv * i, adv * i + adv), 16);
result[i] = adv === 1 ? c << 4 : adv === 2 ? c : adv === 3 ? c >> 4 : c >> 8;
}
return result;
}
}
// Named colors are currently not supported due to the large addition to the xterm.js bundle size
// they would add. In order to support named colors, we would need some way of optionally loading
// additional payloads so startup/download time is not bloated (see #3530).
}
// pad hex output to requested bit width
function pad(n: number, bits: number): string {
const s = n.toString(16);
const s2 = s.length < 2 ? '0' + s : s;
switch (bits) {
case 4:
return s[0];
case 8:
return s2;
case 12:
return (s2 + s2).slice(0, 3);
default:
return s2 + s2;
}
}
/**
* Convert a given color to rgb:../../.. string of `bits` depth.
*/
export function toRgbString(color: [number, number, number], bits: number = 16): string {
const [r, g, b] = color;
return `rgb:${pad(r, bits)}/${pad(g, bits)}/${pad(b, bits)}`;
}
+2 -2
View File
@@ -52,9 +52,9 @@ export class Params implements IParams {
return params;
}
// skip leading sub params
for (let i = (values[0] instanceof Array) ? 1 : 0; i < values.length; ++i) {
for (let i = (Array.isArray(values[0])) ? 1 : 0; i < values.length; ++i) {
const value = values[i];
if (value instanceof Array) {
if (Array.isArray(value)) {
for (let k = 0; k < value.length; ++k) {
params.addSubParam(value[k]);
}
+42 -38
View File
@@ -3,10 +3,9 @@
* @license MIT
*/
import { IOptionsService, ITerminalOptions, IPartialTerminalOptions, FontWeight } from 'common/services/Services';
import { IOptionsService, ITerminalOptions, FontWeight } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { isMac } from 'common/Platform';
import { clone } from 'common/Clone';
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
@@ -14,15 +13,14 @@ import { clone } from 'common/Clone';
// made, apart from the conversion to base64.
export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq';
// TODO: Freeze?
export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
export const DEFAULT_OPTIONS: Readonly<ITerminalOptions> = {
cols: 80,
rows: 24,
cursorBlink: false,
cursorStyle: 'block',
cursorWidth: 1,
customGlyphs: true,
bellSound: DEFAULT_BELL_SOUND,
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
drawBoldTextInBrightColors: true,
fastScrollModifier: 'alt',
@@ -55,57 +53,66 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
convertEol: false,
termName: 'xterm',
cancelEvents: false
});
};
const FONT_WEIGHT_OPTIONS: Extract<FontWeight, string>[] = ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'];
/**
* The set of options that only have an effect when set in the Terminal constructor.
*/
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
export class OptionsService implements IOptionsService {
public serviceBrand: any;
private _options: ITerminalOptions;
public options: ITerminalOptions;
private _onOptionChange = new EventEmitter<string>();
public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; }
constructor(options: IPartialTerminalOptions) {
this.options = clone(DEFAULT_OPTIONS);
for (const k of Object.keys(options)) {
if (k in this.options) {
constructor(options: Partial<ITerminalOptions>) {
// set the default value of each option
this._options = { ...DEFAULT_OPTIONS };
for (const key in options) {
if (key in this._options) {
try {
const newValue = options[k as keyof IPartialTerminalOptions] as any;
this.options[k] = this._sanitizeAndValidateOption(k, newValue);
const newValue = options[key];
this._options[key] = this._sanitizeAndValidateOption(key, newValue);
} catch (e) {
console.error(e);
}
}
}
// set up getters and setters for each option
this.options = this._setupOptions(this._options);
}
private _setupOptions(options: ITerminalOptions): ITerminalOptions {
const copiedOptions = { ... options };
for (const propName in copiedOptions) {
Object.defineProperty(copiedOptions, propName, {
get: () => {
if (!(propName in DEFAULT_OPTIONS)) {
throw new Error(`No option with key "${propName}"`);
}
return this._options[propName];
},
set: (value: any) => {
if (!(propName in DEFAULT_OPTIONS)) {
throw new Error(`No option with key "${propName}"`);
}
value = this._sanitizeAndValidateOption(propName, value);
// Don't fire an option change event if they didn't change
if (this._options[propName] !== value) {
this._options[propName] = value;
this._onOptionChange.fire(propName);
}
}
});
}
return copiedOptions;
}
public setOption(key: string, value: any): void {
if (!(key in DEFAULT_OPTIONS)) {
throw new Error('No option with key "' + key + '"');
}
if (CONSTRUCTOR_ONLY_OPTIONS.includes(key)) {
throw new Error(`Option "${key}" can only be set in the constructor`);
}
if (this.options[key] === value) {
return;
}
value = this._sanitizeAndValidateOption(key, value);
// Don't fire an option change event if they didn't change
if (this.options[key] === value) {
return;
}
this.options[key] = value;
this._onOptionChange.fire(key);
}
private _sanitizeAndValidateOption(key: string, value: any): any {
@@ -160,9 +167,6 @@ export class OptionsService implements IOptionsService {
}
public getOption(key: string): any {
if (!(key in DEFAULT_OPTIONS)) {
throw new Error(`No option with key "${key}"`);
}
return this.options[key];
}
}
+10 -43
View File
@@ -164,6 +164,14 @@ export interface IInstantiationService {
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(t: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
}
export enum LogLevelEnum {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
export const ILogService = createDecorator<ILogService>('LogService');
export interface ILogService {
serviceBrand: undefined;
@@ -190,49 +198,8 @@ export interface IOptionsService {
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
export enum LogLevelEnum {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
export type RendererType = 'dom' | 'canvas';
export interface IPartialTerminalOptions {
altClickMovesCursor?: boolean;
allowTransparency?: boolean;
bellSound?: string;
bellStyle?: 'none' | 'sound' /* | 'visual' | 'both' */;
cols?: number;
cursorBlink?: boolean;
cursorStyle?: 'block' | 'underline' | 'bar';
cursorWidth?: number;
disableStdin?: boolean;
drawBoldTextInBrightColors?: boolean;
fastScrollModifier?: 'alt' | 'ctrl' | 'shift';
fastScrollSensitivity?: number;
fontSize?: number;
fontFamily?: string;
fontWeight?: FontWeight;
fontWeightBold?: FontWeight;
letterSpacing?: number;
lineHeight?: number;
logLevel?: LogLevel;
macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean;
rendererType?: RendererType;
rightClickSelectsWord?: boolean;
rows?: number;
screenReaderMode?: boolean;
scrollback?: number;
scrollSensitivity?: number;
tabStopWidth?: number;
theme?: ITheme;
windowsMode?: boolean;
wordSeparator?: string;
windowOptions?: IWindowOptions;
}
export type RendererType = 'dom' | 'canvas';
export interface ITerminalOptions {
allowProposedApi: boolean;
@@ -241,6 +208,7 @@ export interface ITerminalOptions {
bellSound: string;
bellStyle: 'none' | 'sound' /* | 'visual' | 'both' */;
cols: number;
convertEol: boolean;
cursorBlink: boolean;
cursorStyle: 'block' | 'underline' | 'bar';
cursorWidth: number;
@@ -274,7 +242,6 @@ export interface ITerminalOptions {
[key: string]: any;
cancelEvents: boolean;
convertEol: boolean;
termName: string;
}
+166 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { pollFor, openTerminal, getBrowserType, launchBrowser } from './TestUtils';
import { pollFor, openTerminal, getBrowserType, launchBrowser, writeSync } from './TestUtils';
import { Browser, Page } from 'playwright';
import { IRenderDimensions } from 'browser/renderer/Types';
@@ -386,6 +386,171 @@ describe('InputHandler Integration Tests', function(): void {
});
});
describe('OSC', () => {
describe('OSC 4', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('query single color', async () => {
await writeSync(page, '\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07']);
await writeSync(page, '\x1b]4;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']);
});
it('query multiple colors', async () => {
await writeSync(page, '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:2e2e/3434/3636\x07', '\x1b]4;77;rgb:5f5f/d7d7/5f5f\x07']);
});
it('set & query single color', async () => {
await writeSync(page, '\x1b]4;0;?\x07');
const restore: string[] = await page.evaluate('window._recordedData');
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
// set new color & query
await writeSync(page, '\x1b]4;0;rgb:01/02/03\x07\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07']);
// restore should set old color
await writeSync(page, restore[0] + '\x1b]4;0;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], '\x1b]4;0;rgb:0101/0202/0303\x07', restore[0]]);
});
it('query & set colors mixed', async () => {
await writeSync(page, '\x1b]4;0;?;77;?\x07');
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
// mixed call - change 0, query 43, change 77
await writeSync(page, '\x1b]4;0;rgb:01/02/03;43;?;77;#aabbcc\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;43;rgb:0000/d7d7/afaf\x07']);
await page.evaluate('window._recordedData.length = 0;');
// query new values for 0 + 77
await writeSync(page, '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]4;0;rgb:0101/0202/0303\x07', '\x1b]4;77;rgb:aaaa/bbbb/cccc\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore old values for 0 + 77
await writeSync(page, restore[0] + restore[1] + '\x1b]4;0;?;77;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
});
describe('OSC 4 & 104', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('change & restore single color', async () => {
// test for some random color slots
for (const i of [0, 43, 77, 255]) {
await writeSync(page, `\x1b]4;${i};?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await writeSync(page, `\x1b]4;${i};rgb:01/02/03\x07\x1b]4;${i};?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`]);
// restore slot color
await writeSync(page, `\x1b]104;${i}\x07\x1b]4;${i};?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), [restore[0], `\x1b]4;${i};rgb:0101/0202/0303\x07`, restore[0]]);
await page.evaluate('window._recordedData.length = 0;');
}
});
it('restore multiple at once', async () => {
// change 3 random slots
await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`);
// restore specific slots
await writeSync(page, `\x1b]104;0;43;77\x07` + `\x1b]4;0;?;43;?;77;?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
it('restore full table', async () => {
// change 3 random slots
await writeSync(page, `\x1b]4;0;?;43;?;77;?\x07`);
const restore: string[] = await page.evaluate('window._recordedData');
await page.evaluate('window._recordedData.length = 0;');
await writeSync(page, `\x1b]4;0;rgb:01/02/03;43;#aabbcc;77;#123456\x07`);
// restore all
await writeSync(page, `\x1b]104\x07` + `\x1b]4;0;?;43;?;77;?\x07`);
assert.deepEqual(await page.evaluate('window._recordedData'), restore);
});
});
describe('OSC 10 & 11 + 110 | 111 | 112', () => {
before(async () => {
await page.evaluate('(() => {window._recordedData = []; window._h = term.onData(d => window._recordedData.push(d));})()');
});
after(async () => {
await page.evaluate('window._h.dispose()');
});
beforeEach(async () => {
await page.evaluate('window._recordedData.length = 0;');
});
it('query FG color', async () => {
await writeSync(page, '\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('query BG color', async () => {
await writeSync(page, '\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']);
});
it('query FG & BG color in one call', async () => {
await writeSync(page, '\x1b]10;?;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07', '\x1b]11;rgb:0000/0000/0000\x07']);
});
it('set & query FG', async () => {
await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]10;#ffffff\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07', '\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('set & query BG', async () => {
await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]11;#000000\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07', '\x1b]11;rgb:0000/0000/0000\x07']);
});
it('set & query cursor color', async () => {
await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']);
await writeSync(page, '\x1b]12;#ffffff\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07', '\x1b]12;rgb:ffff/ffff/ffff\x07']);
});
it('set & query FG & BG color in one call', async () => {
await writeSync(page, '\x1b]10;#123456;rgb:aa/bb/cc\x07\x1b]10;?;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1212/3434/5656\x07', '\x1b]11;rgb:aaaa/bbbb/cccc\x07']);
await writeSync(page, '\x1b]10;#ffffff;#000000\x07');
});
it('OSC 110: restore FG color', async () => {
await writeSync(page, '\x1b]10;rgb:1/2/3\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]110\x07\x1b]10;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]10;rgb:ffff/ffff/ffff\x07']);
});
it('OSC 111: restore BG color', async () => {
await writeSync(page, '\x1b]11;rgb:1/2/3\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]111\x07\x1b]11;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]11;rgb:0000/0000/0000\x07']);
});
it('OSC 112: restore cursor color', async () => {
await writeSync(page, '\x1b]12;rgb:1/2/3\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:1111/2222/3333\x07']);
await page.evaluate('window._recordedData.length = 0;');
// restore
await writeSync(page, '\x1b]112\x07\x1b]12;?\x07');
assert.deepEqual(await page.evaluate('window._recordedData'), ['\x1b]12;rgb:ffff/ffff/ffff\x07']);
});
});
});
describe('ESC', () => {
describe('DECRC: Save cursor, ESC 7', () => {
it('should save the absolute cursor position so resizing restores to the correct position', async () => {
+31
View File
@@ -6,6 +6,7 @@
import { assert } from 'chai';
import { pollFor, timeout, writeSync, openTerminal, launchBrowser } from './TestUtils';
import { Browser, Page } from 'playwright';
import { fail } from 'assert';
const APP = 'http://127.0.0.1:3001/test';
@@ -160,6 +161,36 @@ describe('API Integration Tests', function(): void {
assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'dom');
});
describe('options', () => {
it('getter', async () => {
await openTerminal(page);
assert.equal(await page.evaluate(`window.term.options.rendererType`), 'canvas');
assert.equal(await page.evaluate(`window.term.options.cols`), 80);
assert.equal(await page.evaluate(`window.term.options.rows`), 24);
});
it('setter', async () => {
await openTerminal(page);
try {
await page.evaluate('window.term.options.cols = 40');
fail();
} catch {}
try {
await page.evaluate('window.term.options.rows = 20');
fail();
} catch {}
await page.evaluate('window.term.options.scrollback = 1');
assert.equal(await page.evaluate(`window.term.options.scrollback`), 1);
await page.evaluate(`
window.term.options = {
fontSize: 30,
fontFamily: 'Arial'
};
`);
assert.equal(await page.evaluate(`window.term.options.fontSize`), 30);
assert.equal(await page.evaluate(`window.term.options.fontFamily`), 'Arial');
});
});
describe('renderer', () => {
it('foreground', async () => {
await openTerminal(page, { rendererType: 'dom' });
+3 -3
View File
@@ -71,11 +71,11 @@ export function getBrowserType(): playwright.BrowserType<playwright.WebKitBrowse
export function launchBrowser() {
const browserType = getBrowserType();
const options: Record<string, unknown> = {
headless: process.argv.includes('--headless'),
}
headless: process.argv.includes('--headless')
};
const index = process.argv.indexOf('--executablePath');
if(index > 0 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
if (index > 0 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
options.executablePath = process.argv[index + 1];
}
+1 -2
View File
@@ -21,8 +21,7 @@
},
"include": [
"./**/*",
"../../typings/xterm.d.ts",
"../../out/**/*"
"../../typings/xterm.d.ts"
],
"exclude": [
"../../**/*test.ts"
+7 -5
View File
@@ -84,7 +84,7 @@ declare module 'xterm-headless' {
* line height and letter spacing is used. Note that this doesn't work with the DOM renderer
* which renders all characters using the font. The default is true.
*/
customGlyphs?: boolean;
customGlyphs?: boolean;
/**
* Whether input should be disabled.
@@ -1085,18 +1085,20 @@ declare module 'xterm-headless' {
/** Whether the cell has the bold attribute (CSI 1 m). */
isBold(): number;
/** Whether the cell has the inverse attribute (CSI 3 m). */
/** Whether the cell has the italic attribute (CSI 3 m). */
isItalic(): number;
/** Whether the cell has the inverse attribute (CSI 2 m). */
/** Whether the cell has the dim attribute (CSI 2 m). */
isDim(): number;
/** Whether the cell has the underline attribute (CSI 4 m). */
isUnderline(): number;
/** Whether the cell has the inverse attribute (CSI 5 m). */
/** Whether the cell has the blink attribute (CSI 5 m). */
isBlink(): number;
/** Whether the cell has the inverse attribute (CSI 7 m). */
isInverse(): number;
/** Whether the cell has the inverse attribute (CSI 8 m). */
/** Whether the cell has the invisible attribute (CSI 8 m). */
isInvisible(): number;
/** Whether the cell has the strikethrough attribute (CSI 9 m). */
isStrikethrough(): number;
/** Whether the cell is using the RGB foreground color mode. */
isFgRGB(): boolean;
+68 -18
View File
@@ -635,6 +635,29 @@ declare module 'xterm' {
*/
readonly modes: IModes;
/**
* Gets or sets the terminal options. This supports setting multiple options.
*
* @example Get a single option
* ```typescript
* console.log(terminal.options.fontSize);
* ```
*
* @example Set a single option
* ```typescript
* terminal.options.fontSize = 12;
* ```
*
* @example Set multiple options
* ```typescript
* terminal.options = {
* fontSize: 12,
* fontFamily: 'Arial',
* };
* ```
*/
options: ITerminalOptions;
/**
* Natural language strings that can be localized.
*/
@@ -789,9 +812,9 @@ declare module 'xterm' {
deregisterLinkMatcher(matcherId: number): void;
/**
* (EXPERIMENTAL) Registers a link provider, allowing a custom parser to
* be used to match and handle links. Multiple link providers can be used,
* they will be asked in the order in which they are registered.
* Registers a link provider, allowing a custom parser to be used to match
* and handle links. Multiple link providers can be used, they will be asked
* in the order in which they are registered.
* @param linkProvider The link provider to use to detect links.
*/
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
@@ -964,26 +987,31 @@ declare module 'xterm' {
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @deprecated Use `options` instead.
*/
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @deprecated Use `options` instead.
*/
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell' | 'windowsMode'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @deprecated Use `options` instead.
*/
getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @deprecated Use `options` instead.
*/
getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
* @deprecated Use `options` instead.
*/
getOption(key: string): any;
@@ -991,60 +1019,70 @@ declare module 'xterm' {
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'logLevel', value: LogLevel): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'bellStyle', value: null | 'none' | 'visual' | 'sound' | 'both'): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'cursorStyle', value: null | 'block' | 'underline' | 'bar'): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'visualBell' | 'windowsMode', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'theme', value: ITheme): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: 'cols' | 'rows', value: number): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
* @deprecated Use `options` instead.
*/
setOption(key: string, value: any): void;
@@ -1056,6 +1094,14 @@ declare module 'xterm' {
*/
refresh(start: number, end: number): void;
/**
* Clears the texture atlas of the canvas renderer if it's active. Doing this will force a
* redraw of all glyphs which can workaround issues causing the texture to become corrupt, for
* example Chromium/Nvidia has an issue where the texture gets messed up when resuming the OS
* from sleep.
*/
clearTextureAtlas(): void;
/**
* Perform a full reset (RIS, aka '\x1bc').
*/
@@ -1237,12 +1283,12 @@ declare module 'xterm' {
*/
interface IBufferCellPosition {
/**
* The x position within the buffer.
* The x position within the buffer (1-based).
*/
x: number;
/**
* The y position within the buffer.
* The y position within the buffer (1-based).
*/
y: number;
}
@@ -1343,7 +1389,9 @@ declare module 'xterm' {
/**
* The length of the line, all call to getCell beyond the length will result
* in `undefined`.
* in `undefined`. Note that this may exceed columns as the line array may
* not be trimmed after a resize, compare against {@link Terminal.cols} to
* get the actual maximum length of a line.
*/
readonly length: number;
@@ -1444,18 +1492,20 @@ declare module 'xterm' {
/** Whether the cell has the bold attribute (CSI 1 m). */
isBold(): number;
/** Whether the cell has the inverse attribute (CSI 3 m). */
/** Whether the cell has the italic attribute (CSI 3 m). */
isItalic(): number;
/** Whether the cell has the inverse attribute (CSI 2 m). */
/** Whether the cell has the dim attribute (CSI 2 m). */
isDim(): number;
/** Whether the cell has the underline attribute (CSI 4 m). */
isUnderline(): number;
/** Whether the cell has the inverse attribute (CSI 5 m). */
/** Whether the cell has the blink attribute (CSI 5 m). */
isBlink(): number;
/** Whether the cell has the inverse attribute (CSI 7 m). */
isInverse(): number;
/** Whether the cell has the inverse attribute (CSI 8 m). */
/** Whether the cell has the invisible attribute (CSI 8 m). */
isInvisible(): number;
/** Whether the cell has the strikethrough attribute (CSI 9 m). */
isStrikethrough(): number;
/** Whether the cell is using the RGB foreground color mode. */
isFgRGB(): boolean;
+871 -1102
View File
File diff suppressed because it is too large Load Diff