Compare commits

...
309 Commits
Author SHA1 Message Date
Daniel Imms a8fbc8d7a3 Merge pull request #3664 from hello-smile6/patch-1
Add libv86 terminal forwarding to README "Programs using xterm.js"
2022-02-28 05:39:36 -08:00
Daniel Imms c5a48043ae Merge pull request #3663 from childrentime/dev_wlw
Make registerMarker's cursor offset optional
2022-02-28 05:33:51 -08:00
儿时 9b02e950f8 Make registerMarker's cursor offset optional 2022-02-27 13:25:07 +08:00
hello-smile6 14366e7c40 Add libv86 terminal forwarding
https://github.com/hello-smile6/libv86-terminal-forwarding
2022-02-26 16:54:05 -08:00
Daniel Imms e1c149521a Merge pull request #3662 from Tyriar/tyriar/faster_tasks
Make vscode tasks faster on reload
2022-02-25 11:34:52 -08:00
Daniel Imms f5ccf75781 Merge branch 'master' into tyriar/faster_tasks 2022-02-25 11:25:42 -08:00
Daniel Imms 771d5f580a Simplify task names 2022-02-25 11:25:38 -08:00
Daniel Imms 336b45a645 Make vscode tasks faster on reload
See microsoft/vscode#142413
2022-02-25 11:24:40 -08:00
Megan Rogge 0f15a78ba2 Merge pull request #3661 from meganrogge/master
set decoration line height
2022-02-24 13:13:32 -06:00
meganrogge 69d6d53631 Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-24 12:24:37 -06:00
meganrogge 2c76f9bee7 remove height as a factor for lineheight 2022-02-24 12:24:28 -06:00
Megan Rogge 0314d95a32 Merge branch 'xtermjs:master' into master 2022-02-24 12:19:31 -06:00
meganrogge d06a2e1ad7 set line height of decoration 2022-02-24 12:19:05 -06:00
Megan Rogge 134e1ebb5f Merge pull request #3659 from meganrogge/master
use proper y value for clearing markers
2022-02-24 12:18:54 -06:00
meganrogge 3c5b73df8e :Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-24 12:07:59 -06:00
meganrogge ef0312473e re-add i-- so that markers on the same line get disposed of 2022-02-24 12:07:54 -06:00
Megan Rogge 7003c24126 Merge branch 'xtermjs:master' into master 2022-02-24 12:02:25 -06:00
meganrogge 34ec80a322 use ybase + y when clearing markers 2022-02-24 12:02:11 -06:00
Megan Rogge ded6750ff5 Merge pull request #3658 from meganrogge/master
fix marker splice by decrementing index
2022-02-23 16:14:59 -06:00
Megan Rogge d2e34277e8 Merge branch 'xtermjs:master' into master 2022-02-23 16:02:28 -06:00
meganrogge e8579bd655 fix marker splice by decrementing index 2022-02-23 16:02:08 -06:00
Daniel Imms a4f5cbf0cc Merge pull request #3656 from Tyriar/decoration_queue
Queue a refresh when registering a decoration
2022-02-23 13:50:29 -08:00
Daniel Imms b6937d05cd Merge branch 'master' into decoration_queue 2022-02-23 13:41:46 -08:00
Daniel Imms 56b824d379 Queue a refresh when registering a decoration
Part of microsoft/vscode#143798
2022-02-23 13:40:56 -08:00
Megan Rogge 72f7e3fac3 Merge pull request #3655 from meganrogge/master
get Decoration dispose to actually work and revert document role accessibility change
2022-02-22 17:51:13 -06:00
meganrogge d90bbdaa8a Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-22 17:40:43 -06:00
meganrogge 8df23582d2 remove super.dispose call 2022-02-22 17:40:37 -06:00
Megan Rogge e5459d7e24 Merge branch 'master' into master 2022-02-22 14:42:40 -06:00
meganrogge b3d9104136 Revert "Add document role to accessibility tree root"
This reverts commit 3a71b3f11e.
2022-02-22 14:33:30 -06:00
Megan Rogge 912284c24c Merge pull request #3654 from Tyriar/clear_marker
Fix clearMarkers when y=0 and avoid extra indexOf iterations
2022-02-22 14:24:40 -06:00
meganrogge dd86776c6c Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-22 14:18:43 -06:00
meganrogge 44bc9a1f58 don't dispose of marker on dispose of decoration 2022-02-22 14:18:39 -06:00
meganrogge 7b9d775a97 Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-22 14:14:09 -06:00
meganrogge 1da492646c re-add dispose of marker 2022-02-22 14:14:05 -06:00
Megan Rogge e7743c4baf Merge branch 'xtermjs:master' into master 2022-02-22 14:12:42 -06:00
meganrogge cd30f54276 get dispose to work 2022-02-22 14:12:29 -06:00
Megan Rogge a923953373 Merge branch 'master' into clear_marker 2022-02-22 14:11:13 -06:00
Megan Rogge c16b45cc1c Merge pull request #3619 from silamon/htmlserialize
HTML serialization
2022-02-22 14:11:00 -06:00
Megan Rogge 365a07968b Merge branch 'master' into htmlserialize 2022-02-22 10:09:49 -06:00
Daniel Imms da5c24a33f Fix clearMarkers when y=0 and avoid extra indexOf 2022-02-22 06:38:01 -08:00
Simon Lamon 92a61e6506 tests for serializeAsHTML 2022-02-19 13:17:26 +00:00
Megan Rogge e08962b8b8 Merge pull request #3649 from meganrogge/master
on buffer clear, update markers array once each has been disposed of
2022-02-18 17:58:50 -06:00
meganrogge f839a0da8b use splice instead of filter 2022-02-18 17:50:12 -06:00
meganrogge 1efdc2e4ce Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-18 16:11:47 -06:00
meganrogge a4fee6d4b5 get rid of the bane of my existence 2022-02-18 16:11:00 -06:00
Megan Rogge ed24e952f8 Merge branch 'xtermjs:master' into master 2022-02-18 15:38:26 -06:00
meganrogge 5b44deaa30 get rid of markers once they've been disposed via clear 2022-02-18 15:38:07 -06:00
Megan Rogge db1fd267a6 Merge pull request #3644 from meganrogge/master 2022-02-15 21:13:36 -06:00
meganrogge 816f62cebc remove unnecessary prop 2022-02-15 15:36:03 -06:00
meganrogge b8e003768d Merge branch 'master' of https://github.com/meganrogge/xterm.js 2022-02-15 11:57:27 -06:00
meganrogge 7134eaf20c remove trailing space 2022-02-15 11:57:20 -06:00
Megan Rogge b5477f71a8 Merge branch 'xtermjs:master' into master 2022-02-15 11:51:57 -06:00
meganrogge f42e78d832 fix https://github.com/microsoft/vscode/issues/143076 2022-02-15 11:47:41 -06:00
meganrogge 9847dcaacc fix #3641 2022-02-15 11:38:04 -06:00
Daniel Imms d943aa3e5b Merge pull request #3638 from xtermjs/dependabot/npm_and_yarn/addons/xterm-addon-ligatures/follow-redirects-1.14.8
Bump follow-redirects from 1.14.7 to 1.14.8 in /addons/xterm-addon-ligatures
2022-02-15 07:03:35 -08:00
dependabot[bot] 8098fd8d01 Bump follow-redirects in /addons/xterm-addon-ligatures
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-13 12:22:04 +00:00
Simon Lamon 4ca26f7e7b text improvements 2022-02-12 09:42:08 +00:00
Megan Rogge 4da2db8e0b Merge pull request #3637 from meganrogge/master
polish decorations
2022-02-11 17:19:34 -06:00
meganrogge 8dfed8f979 be safe and check if container contains element before removing 2022-02-11 17:08:08 -06:00
meganrogge e720643315 get rid of unused 2022-02-11 17:04:11 -06:00
meganrogge 40e6dba43c cleaner 2022-02-11 17:00:24 -06:00
meganrogge 7d357b941f check if container has child before removing it 2022-02-11 16:48:40 -06:00
meganrogge 3f525fb960 fix #3636 2022-02-11 15:40:44 -06:00
meganrogge de3a09e05f fix #3633 2022-02-11 15:09:40 -06:00
meganrogge d8573e38a8 cleanup dispose 2022-02-11 15:01:07 -06:00
meganrogge e44659e8bd remove unused ID - can just use marker's 2022-02-11 14:56:56 -06:00
meganrogge f160436df8 dispose of decoration on dispose of marker 2022-02-11 14:54:01 -06:00
meganrogge 06357b1184 get rid of some unnecessary code 2022-02-11 14:08:46 -06:00
Megan Rogge d64cbc41d5 Merge branch 'master' into htmlserialize 2022-02-10 10:29:43 -06:00
Megan Rogge 71bfbe434b Merge pull request #3630 from meganrogge/merogge/buffer-marker-decoration
add Decorations
2022-02-09 19:30:49 -06:00
meganrogge 9ed95206c1 try to fix tests 2022-02-09 18:48:41 -06:00
meganrogge 14b71b611e fix test 2022-02-09 18:38:47 -06:00
meganrogge 881bc4f0a8 different approach 2022-02-09 17:37:07 -06:00
meganrogge b3b9f3c7cd add poll for 2022-02-09 17:17:36 -06:00
meganrogge 74246497a4 get term to scroll so it refreshes 2022-02-09 15:29:28 -06:00
meganrogge bf5fed083b fix test 2022-02-09 14:55:24 -06:00
meganrogge c7df6e3c2e inject bufferService 2022-02-09 14:50:02 -06:00
meganrogge 6a79017484 on dispose, remove container 2022-02-09 14:33:33 -06:00
meganrogge e5d4e96aad fix test 2022-02-09 14:30:53 -06:00
meganrogge 93bfc18237 fix test 2022-02-09 12:10:50 -06:00
meganrogge 87675649e1 hide if invalid x 2022-02-09 11:55:17 -06:00
meganrogge e2a402d80e add tests 2022-02-09 11:51:25 -06:00
meganrogge 3e204584d9 refresh on dimensions changed 2022-02-09 10:12:25 -06:00
meganrogge 95c9fe11fb save options in constructor 2022-02-09 10:07:20 -06:00
meganrogge 313cb2fe09 add a container 2022-02-09 09:55:30 -06:00
meganrogge 8e75cecba3 change how rendering happens 2022-02-09 09:44:24 -06:00
meganrogge a2721d43fd Merge branch 'merogge/buffer-marker-decoration' of https://github.com/meganrogge/xterm.js into merogge/buffer-marker-decoration 2022-02-09 08:57:14 -06:00
meganrogge db9bdd46b4 fix some things 2022-02-09 08:56:45 -06:00
Megan RoggeandDaniel Imms 591f32d3eb Update src/browser/Terminal.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2022-02-09 08:51:05 -06:00
meganrogge fb4b19a393 polish 2022-02-09 08:47:10 -06:00
meganrogge ae958c2b12 fix the problem 2022-02-08 21:41:26 -06:00
meganrogge 216849b135 Merge branch 'merogge/buffer-marker-decoration' of https://github.com/meganrogge/xterm.js into merogge/buffer-marker-decoration 2022-02-08 21:31:00 -06:00
meganrogge cfe306fa32 break things 2022-02-08 21:30:11 -06:00
Megan RoggeandDaniel Imms a9b496f06f Update src/browser/services/DecorationService.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2022-02-08 21:18:31 -06:00
meganrogge f2991a5636 cleanup 2022-02-08 21:10:34 -06:00
Megan RoggeandDaniel Imms 53606820a7 Update src/browser/services/DecorationService.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2022-02-08 21:02:15 -06:00
Megan RoggeandDaniel Imms ad9818fb1a Update typings/xterm.d.ts
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2022-02-08 20:29:20 -06:00
meganrogge 88bd33f2d7 bring jumpyness back 2022-02-08 17:27:15 -06:00
meganrogge 5fd7216bf2 add api tests 2022-02-08 15:57:43 -06:00
meganrogge a9722371e1 add decoration test to demo 2022-02-08 15:14:36 -06:00
meganrogge 58c2ba5310 remove empty line 2022-02-08 14:41:33 -06:00
meganrogge d8234affa4 move listener into terminal.ts 2022-02-08 14:35:33 -06:00
meganrogge 571bf9e533 fix bouncing on re render 2022-02-08 14:32:29 -06:00
meganrogge 723e915895 more decorationsService -> singular 2022-02-08 14:25:24 -06:00
meganrogge 3e5ecbf52b more formatting 2022-02-08 14:23:59 -06:00
meganrogge 01b66b756f revert formatting changes 2022-02-08 14:22:37 -06:00
meganrogge 275ae6b2d3 DecorationsService -> DecorationService 2022-02-08 14:20:18 -06:00
meganrogge 094763ea8b Revert changes to webglRenderer 2022-02-08 14:03:49 -06:00
meganrogge 3299315822 remove elt from screen on dispose 2022-02-08 12:26:19 -06:00
Megan Rogge 10d2e66137 Merge branch 'master' into merogge/buffer-marker-decoration 2022-02-08 12:07:13 -06:00
meganrogge 19b9d0eadd cleanup 2022-02-08 12:05:38 -06:00
meganrogge cae9f44f5c remove mention of buffer 2022-02-08 11:45:22 -06:00
meganrogge 8b540c5f3b set initial top value 2022-02-07 16:37:34 -06:00
meganrogge 8bf3cd1102 add to test 2022-02-07 16:06:16 -06:00
meganrogge b226eb2d64 throw for negative x 2022-02-07 14:49:11 -06:00
Megan Rogge e96b709d49 Merge branch 'master' into htmlserialize 2022-02-07 10:44:49 -06:00
meganrogge ac6877aba7 clean up 2022-02-07 10:14:56 -06:00
Megan Rogge a13f11adb4 Merge pull request #3628 from meganrogge/merogge/marker-clear 2022-02-04 21:04:06 -06:00
meganrogge 494aef1c94 remove .only 2022-02-04 19:59:10 -06:00
meganrogge 10adc87a3e add api test 2022-02-04 19:32:34 -06:00
meganrogge 4d7d666c71 add test 2022-02-04 18:00:13 -06:00
meganrogge ac7486856c improve conditional 2022-02-04 17:41:54 -06:00
meganrogge 8999d3948a fix test and improve name 2022-02-04 17:39:37 -06:00
meganrogge f3bd1e24b7 undo formatting change 2022-02-04 16:13:20 -06:00
meganrogge e9913522ac fix buffers not clearing 2022-02-04 16:11:04 -06:00
meganrogge 8f7db4b270 fix buffers not clearing 2022-02-04 16:06:53 -06:00
Megan Rogge 3745496d73 Merge branch 'xtermjs:master' into master 2022-02-04 15:58:01 -06:00
Megan Rogge a64d352d48 Merge branch 'master' into htmlserialize 2022-02-03 23:15:01 -06:00
meganrogge 42d403807c use css class for statics 2022-02-03 13:44:08 -06:00
meganrogge b2f541d4a6 clean up 2022-02-03 13:36:11 -06:00
meganrogge 912c33ca67 get it to wrk 2022-02-03 13:33:30 -06:00
Simon Lamon 4c1722da2c fixup text/plain removal 2022-02-03 18:30:28 +00:00
Simon Lamon 4d70606feb Apply feedback from code review 2022-02-03 18:00:16 +00:00
silamonandDaniel Imms 16257386b1 Apply suggestions from code review
Co-authored-by: Daniel Imms <2193314+Tyriar@users.noreply.github.com>
2022-02-03 18:55:07 +01:00
Daniel Imms a63890e627 Merge pull request #3624 from Tyriar/3617_clear_on_buffer_change
Ensure the glyph vertices are cleared when switching buffers
2022-02-03 08:53:51 -08:00
Daniel Imms 904b491521 Merge branch 'master' into 3617_clear_on_buffer_change 2022-02-03 08:43:53 -08:00
Daniel Imms fea1079b1e Force clear of the glyph renderer when switching
Fixes #3617
2022-02-03 08:33:22 -08:00
Daniel Imms b39d245050 Log parsing data string when using buffers 2022-02-03 08:32:24 -08:00
Daniel Imms b557ebe0a2 Merge pull request #3623 from Tyriar/mocha_update
Update mocha
2022-02-03 07:35:54 -08:00
Daniel Imms bb44dbfc51 Merge branch 'master' into mocha_update 2022-02-03 07:21:49 -08:00
Daniel Imms 490bf81b86 Update mocha 2022-02-03 07:21:00 -08:00
Daniel Imms c3261beaea Merge pull request #3622 from Tyriar/4_17_release
v4.17.0
2022-02-03 07:10:07 -08:00
Daniel Imms 345b8d910b v4.17.0 2022-02-03 07:04:07 -08:00
Daniel Imms 922ee7c48d Merge pull request #3612 from syssecfsu/master
Update README.md to link to WiTTY
2022-02-03 06:48:55 -08:00
meganrogge ad59db9a8a try 2022-02-02 22:33:05 -06:00
meganrogge 49085bbd5b use decorations service instead of decorations render layer 2022-02-02 20:51:28 -06:00
Simon Lamon 93ac9efe92 Use alternate buffer if alternate buffer is active 2022-02-02 19:21:02 +00:00
Simon Lamon bc9e6b49e8 Fix serialize tests 2022-02-02 18:57:41 +00:00
Simon Lamon adedfed2bd Fix serialize tests 2022-02-02 17:44:26 +00:00
Simon Lamon 9473777e43 Fix serialize tests 2022-02-02 17:18:35 +00:00
Megan Rogge a359647683 Merge branch 'master' into htmlserialize 2022-02-02 11:13:59 -06:00
Simon Lamon 09a77998ba improvements for html serialize 2022-02-02 06:27:01 +00:00
meganrogge 5ccb41b9b9 re add height and width 2022-02-01 10:32:09 -06:00
Daniel Imms 131e11b52b Merge pull request #3620 from xtermjs/Tyriar-patch-1
Add keywords to package.json
2022-02-01 08:26:20 -08:00
meganrogge 7d7726817a improve docs 2022-01-31 14:24:24 -06:00
meganrogge fc6245122d more cleanup 2022-01-31 13:08:09 -06:00
meganrogge c5a0711f84 clean up 2022-01-31 12:53:07 -06:00
Daniel Imms 5a3e0612a7 Merge pull request #3621 from felixse/set-text-baseline-for-legacy-edge
fix text baseline for Legacy Edge
2022-01-31 07:41:10 -08:00
Felix Seidl b246318090 fix text baseline for legacy edge 2022-01-30 21:21:07 +01:00
Daniel Imms d807bdac5e Add keywords to addons too 2022-01-30 05:47:58 -08:00
Daniel Imms 1695baa5b0 Add keywords to package.json
I noticed we have no search tags on npmjs.com
2022-01-30 05:43:20 -08:00
meganrogge 9b4801fb43 more polish 2022-01-30 00:36:31 -06:00
meganrogge d7854bc154 remove unused imports 2022-01-30 00:29:02 -06:00
meganrogge f9ac540031 polish 2022-01-30 00:28:01 -06:00
meganrogge 00ce85166e better button 2022-01-29 22:34:16 -06:00
Simon Lamon 6dc813bdff html serialization 2022-01-29 19:49:57 +01:00
meganrogge ebdd07b8ae get button to work 2022-01-29 00:07:51 -06:00
meganrogge c5810e6d54 Take 1 of api 2022-01-27 13:20:02 -06:00
syssecfsu c71252cd23 Update README.md to link to WiTTY 2022-01-22 18:45:30 -05:00
Daniel Imms 6c18b7f2cc Merge pull request #3609 from erulabs/master
Update README.md - add realworld usage @ KubeSail
2022-01-20 07:03:44 -08:00
Seandon Mooy 5e77ad5cb4 Update README.md - add realworld usage @ KubeSail
❤️ 🦞
2022-01-19 23:21:36 -08:00
Daniel Imms e1578813a7 Merge pull request #3606 from xtermjs/dependabot/npm_and_yarn/addons/xterm-addon-ligatures/follow-redirects-1.14.7
Bump follow-redirects from 1.14.3 to 1.14.7 in /addons/xterm-addon-ligatures
2022-01-14 07:18:25 -08:00
dependabot[bot] 21811cb37d Bump follow-redirects in /addons/xterm-addon-ligatures
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.14.7.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.14.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-14 08:59:31 +00:00
Daniel Imms 265227edcb Merge pull request #3604 from Tyriar/exception_3602
Prevent exception being thrown in reflow smaller
2022-01-11 08:25:28 -08:00
Daniel Imms 1d3c1964f9 Merge branch 'master' into exception_3602 2022-01-11 08:12:24 -08:00
Daniel Imms 58b7a4a2d7 Merge pull request #3603 from Tyriar/test_task
Set default vscode test task
2022-01-11 08:12:17 -08:00
Daniel Imms a6b0734fe8 Prevent exception being thrown in reflow smaller
Fixes #3602
2022-01-11 08:11:36 -08:00
Daniel Imms 55cd34c7a5 Set default vscode test task
This allows running the test task via a command/keybinding
2022-01-11 07:50:17 -08:00
Daniel Imms 0e8aeebedb Merge pull request #3599 from silamon/options5
Add options to headless typings
2022-01-11 07:36:30 -08:00
Daniel Imms 1c4d41db86 Merge branch 'master' into options5 2022-01-11 07:27:48 -08:00
Daniel Imms 63c281e405 Merge pull request #3559 from silamon/options3
Resolve perf regression from OptionsService.options
2022-01-11 07:26:43 -08:00
Simon Lamon f03d0858cc fix linting error 2022-01-11 11:58:53 +00:00
Simon Lamon 8bdeb14bc6 add options to headless typings 2022-01-08 10:14:33 +00:00
Simon Lamon ad7fe8f881 fix tests again 2022-01-08 08:51:29 +00:00
Simon Lamon 1dcc1d1baf fix test and replace options with rawOptions 2022-01-08 08:20:50 +00:00
Daniel Imms 0029bc3b42 Merge pull request #3598 from LabhanshAgrawal/ligatures
Bump font-ligatures to 1.4.1
2022-01-07 09:14:21 -08:00
Labhansh Agrawal 0b73a8d469 Bump font-ligatures to 1.4.1 2022-01-07 11:51:24 +05:30
Daniel Imms 3a2b97e0f7 Merge pull request #3597 from Aaaaash/chore/add-opensumi
Add opensumi to the list of users
2022-01-06 07:42:03 -08:00
大表哥 9bcc98beb9 Add opensumi to the list of users 2022-01-06 10:10:10 +08:00
Daniel Imms 1fb11707a5 Merge pull request #3596 from wesbos/patch-1
Extend TLD matching to 14 chars
2022-01-05 13:54:05 -08:00
Wes Bos 84548d5d7b Update WebLinksAddon.ts 2022-01-05 16:29:39 -05:00
Wes Bos 6f666f56c1 Extend TLD matching to 14 chars
I ran into this issue with hyper wasn't highlighting my `.localhost` domains, which is a legitimate TLD and used by caddy server to allow SSL certs locally.

Then I went down the rabbit hole of TLDs and found currently the longest TLDs are 14 chars which include .BANANAREPUBLIC, .COOKINGCHANNEL, .KERRYLOGISTICS, .WEATHERCHANNEL

I'm not regex expert, but I *think* this should work? 😀
2022-01-05 16:28:05 -05:00
Daniel Imms 2866977941 Merge pull request #3595 from xtermjs/Tyriar-patch-1
Fix dependents link
2022-01-05 12:58:50 -08:00
Daniel Imms c087592e2c Fix dependents link 2022-01-05 12:58:29 -08:00
Simon Lamon 65182176b9 remove proxy 2021-12-25 14:02:09 +00:00
Daniel Imms c8123d911e Merge pull request #3591 from Tyriar/3590
Disable macOS integration tests
2021-12-22 11:00:40 -08:00
Daniel Imms cacda99707 Disable macOS integration tests
Fixes #3590
2021-12-22 10:45:49 -08:00
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
Simon Lamon bac4eba35e Remove the test referencing publicOptions 2021-12-21 19:41:56 +00:00
Simon Lamon 625df61489 clean up 2021-12-21 19:29:18 +00:00
Simon Lamon ccca58bb95 Merge branch 'master' into options3 2021-12-21 19:22:41 +00: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 9557b9132b Merge branch 'master' into options3 2021-12-17 11:13:23 -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
Simon Lamon d6d540e748 use a proxy 2021-11-20 14:58:07 +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
Eugeny f0303072f3 Merge branch 'master' into fix-duplicate-ime-input 2021-11-02 14:42:32 +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 7abfccc23e Merge branch 'master' into master 2021-10-25 03:16:38 -07:00
Jörg Breitbart 0871ace481 create minified color names module 2021-10-25 00:30:26 +02: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
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
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 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
87 changed files with 2541 additions and 568 deletions
+8 -3
View File
@@ -11,10 +11,14 @@
{
"type": "npm",
"script": "test",
"group": "test",
"group":{
"kind": "test",
"isDefault": true
},
"problemMatcher": []
},
{
"label": "watch",
"type": "npm",
"script": "watch",
"group": "build",
@@ -25,9 +29,10 @@
}
},
{
"label": "start",
"type": "npm",
"script": "start",
"dependsOn": "npm: watch",
"dependsOn": "watch",
"group": "build",
"isBackground": true,
"problemMatcher": [],
@@ -37,7 +42,7 @@
},
{
"label": "Start demo",
"dependsOn": "npm: start",
"dependsOn": "start",
"group": {
"kind": "build",
"isDefault": true
+6 -2
View File
@@ -179,13 +179,17 @@ 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)
- [**OpenSumi**](https://github.com/opensumi/core): A framework helps you quickly build Cloud or Desktop IDE products.
- [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps
- [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions.
- [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization.
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
+5
View File
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-attach.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
+5
View File
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-fit.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-ligatures",
"version": "0.5.1",
"version": "0.5.2",
"description": "Add support for programming ligatures to xterm.js",
"author": {
"name": "The xterm.js authors",
@@ -25,14 +25,14 @@
"keywords": [
"font",
"ligature",
"terminal",
"xterm",
"xterm.js",
"terminal"
"xterm.js"
],
"license": "MIT",
"dependencies": {
"font-finder": "^1.1.0",
"font-ligatures": "^1.4.0"
"font-ligatures": "^1.4.1"
},
"devDependencies": {
"@types/sinon": "^5.0.1",
+7 -7
View File
@@ -67,9 +67,9 @@ fd-slicer@~1.1.0:
pend "~1.2.0"
follow-redirects@^1.14.0:
version "1.14.3"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
version "1.14.8"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
font-finder@^1.0.3:
version "1.0.4"
@@ -86,10 +86,10 @@ font-finder@^1.1.0:
get-system-fonts "^2.0.0"
promise-stream-reader "^1.0.1"
font-ligatures@^1.4.0:
version "1.4.0"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.0.tgz#6a7b370d96be1358dddfad67830e82fbfd59e6dc"
integrity sha512-n7DFnnEpJ0NrVoLqZIL4tMGVs+CnFwQc92m80LWyrbgAFO4x234+t2/H9o4eOYA1eh6ta9dZAEEsJAwsBdNezA==
font-ligatures@^1.4.1:
version "1.4.1"
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz#e8c9df7cf6bee80c0cca4a9a578618c7b1f5d8de"
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
dependencies:
font-finder "^1.0.3"
lru-cache "^6.0.0"
+6 -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/"
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-search.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
+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;
@@ -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" },
+6 -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/"
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-serialize.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
@@ -0,0 +1,205 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { SerializeAddon } from './SerializeAddon';
import { Terminal } from 'browser/public/Terminal';
import { ColorManager } from 'browser/ColorManager';
import { SelectionModel } from 'browser/selection/SelectionModel';
import { IBufferService } from 'common/services/Services';
function sgr(...seq: string[]): string {
return `\x1b[${seq.join(';')}m`;
}
function writeP(terminal: Terminal, data: string | Uint8Array): Promise<void> {
return new Promise(r => terminal.write(data, r));
}
class TestSelectionService {
private _model: SelectionModel;
private _hasSelection: boolean = false;
constructor(
bufferService: IBufferService
) {
this._model = new SelectionModel(bufferService);
}
public get model(): SelectionModel { return this._model; }
public get hasSelection(): boolean { return this._hasSelection; }
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
public setSelection(col: number, row: number, length: number): void {
this._model.selectionStart = [col, row];
this._model.selectionStartLength = length;
this._hasSelection = true;
}
}
describe('xterm-addon-serialize html', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: jsdom.DOMWindow;
let serializeAddon: SerializeAddon;
let terminal: Terminal;
let selectionService: any;
before(() => {
serializeAddon = new SerializeAddon();
});
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
fillRect(): void { },
getImageData(): any {
return { data: [0, 0, 0, 0xFF] };
}
});
terminal = new Terminal({ cols: 10, rows: 2 });
terminal.loadAddon(serializeAddon);
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
cm = new ColorManager(document, false);
(terminal as any)._core._colorManager = cm;
(terminal as any)._core._selectionService = selectionService;
});
it('empty terminal with selection turned off', () => {
const output = serializeAddon.serializeAsHTML();
assert.notEqual(output, '');
assert.equal((output.match(new RegExp('<div><span> {10}</span><\/div>', 'g')) || []).length, 2);
});
it('empty terminal with no selection', () => {
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal(output, '');
});
it('basic terminal with selection', async () => {
await writeP(terminal, ' terminal ');
terminal.select(1, 0, 8);
const output = serializeAddon.serializeAsHTML({
onlySelection: true
});
assert.equal((output.match(new RegExp('<div><span>terminal<\/span><\/div>', 'g')) || []).length, 1, output);
});
it('cells with bold styling', async () => {
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with italic styling', async () => {
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-style: italic;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with inverse styling', async () => {
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #000000; background-color: #BFBFBF;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with underline styling', async () => {
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: underline;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with invisible styling', async () => {
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'visibility: hidden;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with dim styling', async () => {
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'opacity: 0.5;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with strikethrough styling', async () => {
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with combined styling', async () => {
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold;\'> <\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'font-weight: bold; text-decoration: line-through;\'>termi<\/span>', 'g')) || []).length, 1, output);
assert.equal((output.match(new RegExp('<span style=\'text-decoration: line-through;\'>nal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with color styling', async () => {
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('cells with background styling', async () => {
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('<span style=\'background-color: #00ff00;\'>terminal<\/span>', 'g')) || []).length, 1, output);
});
it('empty terminal with default options', async () => {
const output = serializeAddon.serializeAsHTML();
assert.equal((output.match(new RegExp('color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
it('empty terminal with custom options', async () => {
terminal.options.fontFamily = 'verdana';
terminal.options.fontSize = 20;
terminal.options.theme = {
foreground: '#ff00ff',
background: '#00ff00'
};
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(new RegExp('color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;', 'g')) || []).length, 1, output);
});
it('empty terminal with background included', async () => {
const output = serializeAddon.serializeAsHTML({
includeGlobalBackground: true
});
assert.equal((output.match(new RegExp('color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;', 'g')) || []).length, 1, output);
});
});
@@ -5,8 +5,8 @@
* (EXPERIMENTAL) This Addon is still under development
*/
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
import { IColorSet } from 'browser/Types';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
@@ -19,18 +19,25 @@ abstract class BaseSerializeHandler {
) {
}
public serialize(startRow: number, endRow: number): string {
public serialize(range: IBufferRange): string {
// we need two of them to flip between old and new cell
const cell1 = this._buffer.getNullCell();
const cell2 = this._buffer.getNullCell();
let oldCell = cell1;
const startRow = range.start.x;
const endRow = range.end.x;
const startColumn = range.start.y;
const endColumn = range.end.y;
this._beforeSerialize(endRow - startRow, startRow, endRow);
for (let row = startRow; row < endRow; row++) {
for (let row = startRow; row <= endRow; row++) {
const line = this._buffer.getLine(row);
if (line) {
for (let col = 0; col < line.length; col++) {
const startLineColumn = row !== range.start.x ? 0 : startColumn;
const endLineColumn = row !== range.end.x ? line.length : endColumn;
for (let col = startLineColumn; col < endLineColumn; col++) {
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
if (!c) {
console.warn(`Can't get cell at row=${row}, col=${col}`);
@@ -40,7 +47,7 @@ abstract class BaseSerializeHandler {
oldCell = c;
}
}
this._rowEnd(row, row === endRow - 1);
this._rowEnd(row, row === endRow);
}
this._afterSerialize();
@@ -403,7 +410,35 @@ export class SerializeAddon implements ITerminalAddon {
const maxRows = buffer.length;
const handler = new StringSerializeHandler(buffer, terminal);
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
return handler.serialize(maxRows - correctRows, maxRows);
return handler.serialize({
start: { x: maxRows - correctRows, y: 0 },
end: { x: maxRows - 1, y: terminal.cols }
});
}
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
const buffer = terminal.buffer.active;
const handler = new HTMLSerializeHandler(buffer, terminal, options);
const onlySelection = options.onlySelection ?? false;
if (!onlySelection) {
const maxRows = buffer.length;
const scrollback = options.scrollback;
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
return handler.serialize({
start: { x: maxRows - correctRows, y: 0 },
end: { x: maxRows - 1, y: terminal.cols }
});
}
const selection = this._terminal?.getSelectionPosition();
if (selection !== undefined) {
return handler.serialize({
start: { x: selection.startRow, y: selection.startColumn },
end: { x: selection.endRow, y: selection.endColumn }
});
}
return '';
}
private _serializeModes(terminal: Terminal): string {
@@ -460,6 +495,14 @@ export class SerializeAddon implements ITerminalAddon {
return content;
}
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
return this._serializeBufferAsHTML(this._terminal, options || {});
}
public dispose(): void { }
}
@@ -469,3 +512,150 @@ interface ISerializeOptions {
excludeModes?: boolean;
excludeAltBuffer?: boolean;
}
interface IHTMLSerializeOptions {
scrollback: number;
onlySelection: boolean;
includeGlobalBackground: boolean;
}
export class HTMLSerializeHandler extends BaseSerializeHandler {
private _currentRow: string = '';
private _htmlContent = '';
private _colors: IColorSet;
constructor(
buffer: IBuffer,
private readonly _terminal: Terminal,
private readonly _options: Partial<IHTMLSerializeOptions>
) {
super(buffer);
// https://github.com/xtermjs/xterm.js/issues/3601
this._colors = (_terminal as any)._core._colorManager.colors;
}
private _padStart(target: string, targetLength: number, padString: string): string {
targetLength = targetLength >> 0;
padString = padString ?? ' ';
if (target.length > targetLength) {
return target;
}
targetLength = targetLength - target.length;
if (targetLength > padString.length) {
padString += padString.repeat(targetLength / padString.length);
}
return padString.slice(0, targetLength) + target;
}
protected _beforeSerialize(rows: number, start: number, end: number): void {
this._htmlContent += '<html><body><!--StartFragment--><pre>';
let foreground = '#000000';
let background = '#ffffff';
if (this._options.includeGlobalBackground ?? false) {
foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
background = this._terminal.options.theme?.background ?? '#000000';
}
const globalStyleDefinitions = [];
globalStyleDefinitions.push('color: ' + foreground + ';');
globalStyleDefinitions.push('background-color: ' + background + ';');
globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');
globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');
this._htmlContent += '<div style=\'' + globalStyleDefinitions.join(' ') + '\'>';
}
protected _afterSerialize(): void {
this._htmlContent += '</div>';
this._htmlContent += '</pre><!--EndFragment--></body></html>';
}
protected _rowEnd(row: number, isLastRow: boolean): void {
this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';
this._currentRow = '';
}
private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {
const color = isFg ? cell.getFgColor() : cell.getBgColor();
if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
const rgb = [
(color >> 16) & 255,
(color >> 8) & 255,
(color ) & 255
];
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
}
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
return this._colors.ansi[color].css;
}
return undefined;
}
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
const content: string[] = [];
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
if (fgChanged || bgChanged || flagsChanged) {
const fgHexColor = this._getHexColor(cell, true);
if (fgHexColor) {
content.push('color: ' + fgHexColor + ';');
}
const bgHexColor = this._getHexColor(cell, false);
if (bgHexColor) {
content.push('background-color: ' + bgHexColor + ';');
}
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
if (cell.isBold()) { content.push('font-weight: bold;'); }
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
if (cell.isItalic()) { content.push('font-style: italic;'); }
if (cell.isDim()) { content.push('opacity: 0.5;'); }
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
return content;
}
return undefined;
}
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
const isPlaceHolderCell = cell.getWidth() === 0;
if (isPlaceHolderCell) {
return;
}
// this cell don't have content
const isEmptyCell = cell.getChars() === '';
const styleDefinitions = this._diffStyle(cell, oldCell);
// handles style change
if (styleDefinitions) {
this._currentRow += styleDefinitions.length === 0 ?
'</span><span>' :
'</span><span style=\'' + styleDefinitions.join(' ') + '\'>';
}
// handles actual content
if (isEmptyCell) {
this._currentRow += ' ';
} else {
this._currentRow += cell.getChars();
}
}
protected _serializeString(): string {
return this._htmlContent;
}
}
@@ -14,6 +14,9 @@
"paths": {
"common/*": [
"../../../src/common/*"
],
"browser/*": [
"../../../src/browser/*"
]
},
"strict": true,
@@ -28,6 +31,9 @@
"references": [
{
"path": "../../../src/common"
},
{
"path": "../../../src/browser"
}
]
}
@@ -24,14 +24,23 @@ 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.
*
*
* 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(options?: ISerializeOptions): string;
/**
* Serializes terminal content as HTML, which can be written to the clipboard using the
* `text/html` mimetype. For applications that support it, the pasted text should then retain
* its colors/styles.
*
* @param options Custom options to allow control over what gets serialized.
*/
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string;
/**
* Disposes the addon.
*/
@@ -56,4 +65,24 @@ declare module 'xterm-addon-serialize' {
*/
excludeAltBuffer?: boolean;
}
export interface IHTMLSerializeOptions {
/**
* 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. This setting is ignored if {@link IHTMLSerializeOptions.onlySelection} is true.
*/
scrollback: number;
/**
* Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML.
* False by default.
*/
onlySelection: boolean;
/**
* Whether to include the global background of the terminal. False by default.
*/
includeGlobalBackground: boolean;
}
}
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-unicode11.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
+6 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-web-links",
"version": "0.4.0",
"version": "0.5.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
@@ -9,6 +9,11 @@
"types": "typings/xterm-addon-web-links.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
@@ -10,7 +10,7 @@ const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const tldClause = '([a-z\\.]{2,18})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
@@ -43,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 {
@@ -62,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;
+7 -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/"
@@ -9,6 +9,12 @@
"types": "typings/xterm-addon-webgl.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"terminal",
"webgl",
"xterm",
"xterm.js"
],
"scripts": {
"build": "../../node_modules/.bin/tsc -p .",
"prepackage": "npm run build",
+24 -19
View File
@@ -299,32 +299,37 @@ export class GlyphRenderer {
return this._colors.ansi[idx];
}
public onResize(): void {
public clear(force?: boolean): void {
const terminal = this._terminal;
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
// Update vertices
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
if (this._vertices.count !== newCount) {
this._vertices.count = newCount;
this._vertices.attributes = new Float32Array(newCount);
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
}
let i = 0;
for (let y = 0; y < terminal.rows; y++) {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 8] = x / terminal.cols;
this._vertices.attributes[i + 9] = y / terminal.rows;
i += INDICES_PER_CELL;
}
// Don't clear if not forced and the array length is correct
if (!force && this._vertices.count === newCount) {
return;
}
// Clear vertices
this._vertices.count = newCount;
this._vertices.attributes = new Float32Array(newCount);
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
}
let i = 0;
for (let y = 0; y < terminal.rows; y++) {
for (let x = 0; x < terminal.cols; x++) {
this._vertices.attributes[i + 8] = x / terminal.cols;
this._vertices.attributes[i + 9] = y / terminal.rows;
i += INDICES_PER_CELL;
}
}
}
public onResize(): void {
const gl = this._gl;
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
this.clear();
}
public setColors(): void {
}
@@ -247,6 +247,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
}
public clear(): void {
this._model.clear();
this._glyphRenderer.clear(true);
for (const l of this._renderLayers) {
l.reset(this._terminal);
}
@@ -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;
@@ -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;
+18 -17
View File
@@ -105,22 +105,23 @@ jobs:
- script: xvfb-run --auto-servernum -- bash -c "yarn test-api-firefox --headless --forbid-only"
displayName: 'Integration tests (Firefox)'
- job: macOS_IntegrationTests
pool:
vmImage: 'macOS-10.15'
steps:
- task: NodeTool@0
inputs:
versionSpec: '14.x'
displayName: 'Install Node.js'
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build'
- script: yarn test-api-chromium --headless --forbid-only
displayName: 'Integration tests (Chromium)'
- script: yarn test-api-firefox --headless --forbid-only
displayName: 'Integration tests (Firefox)'
- script: yarn test-api-webkit --headless --forbid-only
displayName: 'Integration tests (Webkit)'
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
# - job: macOS_IntegrationTests
# pool:
# vmImage: 'macOS-10.15'
# steps:
# - task: NodeTool@0
# inputs:
# versionSpec: '14.x'
# displayName: 'Install Node.js'
# - script: yarn --frozen-lockfile
# displayName: 'Install dependencies and build'
# - script: yarn test-api-chromium --headless --forbid-only
# displayName: 'Integration tests (Chromium)'
# - script: yarn test-api-firefox --headless --forbid-only
# displayName: 'Integration tests (Firefox)'
# - script: yarn test-api-webkit --headless --forbid-only
# displayName: 'Integration tests (Webkit)'
- job: Windows_IntegrationTests
pool:
@@ -143,7 +144,7 @@ jobs:
- macOS
- Windows
- Linux_IntegrationTests
- macOS_IntegrationTests
# - macOS_IntegrationTests
- Windows_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool:
+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], {
+7 -1
View File
@@ -133,7 +133,8 @@
cursor: default;
}
.xterm.xterm-cursor-pointer {
.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
cursor: pointer;
}
@@ -172,3 +173,8 @@
.xterm-strikethrough {
text-decoration: line-through;
}
.xterm-screen .xterm-decoration-container .xterm-decoration {
z-index: 6;
position: absolute;
}
+26 -1
View File
@@ -147,8 +147,10 @@ if (document.location.pathname === '/test') {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
document.getElementById('load-test').addEventListener('click', loadTest);
document.getElementById('add-decoration').addEventListener('click', addDecoration);
}
function createTerminal(): void {
@@ -293,7 +295,7 @@ function initOptions(term: TerminalType): void {
rendererType: ['dom', 'canvas'],
wordSeparator: null
};
const options = Object.keys(term.options);
const options = Object.getOwnPropertyNames(term.options);
const booleanOptions = [];
const numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
@@ -447,6 +449,21 @@ function serializeButtonHandler(): void {
}
}
function htmlSerializeButtonHandler(): void {
const output = addons.serialize.instance.serializeAsHTML();
document.getElementById('htmlserialize-output').innerText = output;
// Deprecated, but the most supported for now.
function listener(e: any) {
e.clipboardData.setData("text/html", output);
e.preventDefault();
}
document.addEventListener("copy", listener);
document.execCommand("copy");
document.removeEventListener("copy", listener);
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
}
function writeCustomGlyphHandler() {
term.write('\n\r');
@@ -525,3 +542,11 @@ function loadTest() {
term._core._onData.fire('\x03');
});
}
function addDecoration() {
const marker = term.addMarker(1);
const decoration = term.registerDecoration({ marker });
decoration.onRender(() => {
decoration.element.style.backgroundColor = 'red';
});
}
+5
View File
@@ -49,6 +49,10 @@
<button id="serialize">Serialize the content of terminal</button>
<label><input type="checkbox" id="write-to-terminal">Write back to terminal</label>
<div><pre id="serialize-output"></pre></div>
<button id="htmlserialize">Serialize the content of terminal in HTML</button>
<span id="htmlserialize-output-result"></span>
<div><pre id="htmlserialize-output"></pre></div>
</div>
</div>
<div id="style" class="tabContent">
@@ -64,6 +68,7 @@
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
<button id="add-decoration" title="Add a decoration to the terminal">Decoration</button>
</div>
</div>
</div>
+17 -2
View File
@@ -1,12 +1,27 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.14.1",
"version": "4.17.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"keywords": [
"cli",
"command-line",
"console",
"pty",
"shell",
"ssh",
"styles",
"terminal-emulator",
"terminal",
"tty",
"vt100",
"webgl",
"xterm"
],
"scripts": {
"prepackage": "npm run build",
"package": "webpack",
@@ -57,7 +72,7 @@
"express-ws": "^5.0.2",
"glob": "^7.2.0",
"jsdom": "^18.0.1",
"mocha": "^9.1.3",
"mocha": "^9.2.0",
"mustache": "^4.2.0",
"node-pty": "^0.10.1",
"nyc": "^15.1.0",
+1 -1
View File
@@ -53,8 +53,8 @@ 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(
+1 -1
View File
@@ -150,7 +150,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
}
// Restart the tooltip timeout
this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.options.linkTooltipHoverDuration);
this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.rawOptions.linkTooltipHoverDuration);
}
private _onTooltip(e: MouseEvent): void {
+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 {
+78 -26
View File
@@ -37,24 +37,25 @@ import * as Strings from 'browser/LocalizableStrings';
import { SoundService } from 'browser/services/SoundService';
import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } 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';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService, IDecorationService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { IBuffer } from 'common/buffer/Types';
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';
import { DecorationService } from 'browser/services/DecorationService';
// Let it work inside Node.js for automated testing purposes.
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
@@ -74,9 +75,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
@@ -111,6 +109,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
public linkifier: ILinkifier;
public linkifier2: ILinkifier2;
public viewport: IViewport | undefined;
public decorationService: IDecorationService;
private _compositionHelper: ICompositionHelper | undefined;
private _mouseZoneManager: IMouseZoneManager | undefined;
private _accessibilityManager: AccessibilityManager | undefined;
@@ -160,6 +159,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.linkifier = this._instantiationService.createInstance(Linkifier);
this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2));
this.decorationService = this.register(this._instantiationService.createInstance(DecorationService));
// Setup InputHandler listeners
this.register(this._inputHandler.onRequestBell(() => this.bell()));
@@ -167,7 +167,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
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));
@@ -177,17 +177,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 {
@@ -263,7 +301,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.viewport?.syncScrollArea();
break;
case 'screenReaderMode':
if (this.optionsService.options.screenReaderMode) {
if (this.optionsService.rawOptions.screenReaderMode) {
if (!this._accessibilityManager && this._renderService) {
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
}
@@ -274,7 +312,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
break;
case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'theme':
this._setTheme(this.optionsService.options.theme);
this._setTheme(this.optionsService.rawOptions.theme);
break;
}
}
@@ -537,8 +575,9 @@ 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.decorationService.attachToDom(this.screenElement, this._renderService, this._bufferService);
// This event listener must be registered aftre MouseZoneManager is created
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
@@ -718,7 +757,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
this.register(this.coreMouseService.onProtocolChange(events => {
// apply global changes on events
if (events) {
if (this.optionsService.options.logLevel === 'debug') {
if (this.optionsService.rawOptions.logLevel === 'debug') {
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
}
this.element!.classList.add('enable-mouse-events');
@@ -820,7 +859,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// normal viewport scrolling
// conditionally stop event, if the viewport still had rows to scroll within
if (!this.viewport!.onWheel(ev)) {
if (this.viewport!.onWheel(ev)) {
return this.cancel(ev);
}
}, { passive: false }));
@@ -963,6 +1002,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
return this.decorationService!.registerDecoration(decorationOptions);
}
/**
* Gets whether the terminal has an active selection.
*/
@@ -1090,7 +1133,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// other listeners. When screen reader mode is enabled, this could cause issues if the event
// is handled at a higher level, this is a compromise in order to echo keys to the screen
// reader.
if (!this.optionsService.options.screenReaderMode) {
if (!this.optionsService.rawOptions.screenReaderMode) {
return this.cancel(event, true);
}
@@ -1169,6 +1212,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;
}
@@ -1181,11 +1228,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.rawOptions.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);
@@ -1250,6 +1301,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
// Don't clear if it's already clear
return;
}
this.buffer.clearMarkers();
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
this.buffer.lines.length = 1;
this.buffer.ydisp = 0;
+13 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
@@ -102,6 +102,9 @@ export class MockTerminal implements ITerminal {
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
throw new Error('Method not implemented.');
}
public hasSelection(): boolean {
throw new Error('Method not implemented.');
}
@@ -254,6 +257,9 @@ export class MockBuffer implements IBuffer {
public getWhitespaceCell(attr?: IAttributeData): ICellData {
throw new Error('Method not implemented.');
}
public clearMarkers(): void {
throw new Error('Method not implemented.');
}
}
export class MockRenderer implements IRenderer {
@@ -280,6 +286,9 @@ export class MockRenderer implements IRenderer {
public setColors(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
public onResize(cols: number, rows: number): void { }
public onCharSizeChanged(): void { }
public onBlur(): void { }
@@ -419,6 +428,9 @@ export class MockRenderService implements IRenderService {
public dispose(): void {
throw new Error('Method not implemented.');
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
throw new Error('Method not implemented.');
}
}
export class MockCharacterJoinerService implements ICharacterJoinerService {
+3 -3
View File
@@ -3,11 +3,11 @@
* @license MIT
*/
import { IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IEvent } from 'common/EventEmitter';
import { ICoreTerminal, CharData, ITerminalOptions } from 'common/Types';
import { IMouseService, IRenderService } from './services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IBuffer } from 'common/buffer/Types';
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
export interface ITerminal extends IPublicTerminal, ICoreTerminal {
@@ -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;
@@ -62,6 +61,7 @@ export interface IPublicTerminal extends IDisposable {
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
deregisterCharacterJoiner(joinerId: number): void;
addMarker(cursorYOffset: number): IMarker | undefined;
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
hasSelection(): boolean;
getSelection(): string;
getSelectionPosition(): ISelectionPosition | undefined;
+7 -7
View File
@@ -110,7 +110,7 @@ export class Viewport extends Disposable implements IViewport {
}
// Update scroll bar width
if (this._optionsService.options.scrollback === 0) {
if (this._optionsService.rawOptions.scrollback === 0) {
this.scrollBarWidth = 0;
} else {
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
@@ -153,7 +153,7 @@ export class Viewport extends Disposable implements IViewport {
}
// If the scroll bar visibility changed
if (this._lastHadScrollBar !== (this._optionsService.options.scrollback > 0)) {
if (this._lastHadScrollBar !== (this._optionsService.rawOptions.scrollback > 0)) {
this._refresh(immediate);
}
}
@@ -220,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;
}
@@ -241,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;
}
@@ -259,15 +259,15 @@ export class Viewport extends Disposable implements IViewport {
}
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
const modifier = this._optionsService.options.fastScrollModifier;
const modifier = this._optionsService.rawOptions.fastScrollModifier;
// Multiply the scroll speed when the modifier is down
if ((modifier === 'alt' && ev.altKey) ||
(modifier === 'ctrl' && ev.ctrlKey) ||
(modifier === 'shift' && ev.shiftKey)) {
return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
return amount * this._optionsService.rawOptions.fastScrollSensitivity * this._optionsService.rawOptions.scrollSensitivity;
}
return amount * this._optionsService.options.scrollSensitivity;
return amount * this._optionsService.rawOptions.scrollSensitivity;
}
/**
+2 -2
View File
@@ -217,8 +217,8 @@ export class CompositionHelper {
this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.fontFamily = this._optionsService.options.fontFamily;
this._compositionView.style.fontSize = this._optionsService.options.fontSize + 'px';
this._compositionView.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._compositionView.style.fontSize = this._optionsService.rawOptions.fontSize + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this._compositionView.getBoundingClientRect();
+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;
+56 -4
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, IDecorationOptions, IDecoration } from 'xterm';
import { ITerminal } from 'browser/Types';
import { Terminal as TerminalCore } from 'browser/Terminal';
import * as Strings from 'browser/LocalizableStrings';
@@ -12,20 +12,53 @@ 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 = { ... this._core.options };
const getter = (propName: string): any => {
return this._core.options[propName];
};
const setter = (propName: string, value: any): void => {
this._checkReadonlyOptions(propName);
this._core.options[propName] = value;
};
for (const propName in this._core.options) {
const desc = {
get: getter.bind(this, propName),
set: setter.bind(this, propName)
};
Object.defineProperty(this._publicOptions, propName, desc);
}
}
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 {
if (!this._core.optionsService.options.allowProposedApi) {
if (!this._core.optionsService.rawOptions.allowProposedApi) {
throw new Error('You must set the allowProposedApi option to true to use proposed API');
}
}
@@ -90,7 +123,12 @@ export class Terminal implements ITerminalApi {
};
}
public get options(): ITerminalOptions {
return this._core.options;
return this._publicOptions;
}
public set options(options: ITerminalOptions) {
for (const propName in options) {
this._publicOptions[propName] = options[propName];
}
}
public blur(): void {
this._core.blur();
@@ -128,11 +166,16 @@ export class Terminal implements ITerminalApi {
this._checkProposedApi();
this._core.deregisterCharacterJoiner(joinerId);
}
public registerMarker(cursorYOffset: number): IMarker | undefined {
public registerMarker(cursorYOffset: number = 0): IMarker | undefined {
this._checkProposedApi();
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
this._checkProposedApi();
this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0);
return this._core.registerDecoration(decorationOptions);
}
public addMarker(cursorYOffset: number): IMarker | undefined {
return this.registerMarker(cursorYOffset);
}
@@ -216,6 +259,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 {
@@ -242,4 +286,12 @@ export class Terminal implements ITerminalApi {
}
}
}
private _verifyPositiveIntegers(...values: number[]): void {
for (const value of values) {
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) {
throw new Error('This API only accepts positive integers');
}
}
}
}
+12 -17
View File
@@ -112,7 +112,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = acquireCharAtlas(this._optionsService.options, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}
@@ -267,7 +267,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.options.customGlyphs !== false) {
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
}
@@ -315,7 +315,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
}
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
@@ -325,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);
@@ -361,7 +356,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
@@ -375,7 +370,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
@@ -391,7 +386,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
// Draw custom characters if applicable
let drawSuccess = false;
if (this._optionsService.options.customGlyphs !== false) {
if (this._optionsService.rawOptions.customGlyphs !== false) {
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
}
@@ -426,14 +421,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? this._optionsService.options.fontWeightBold : this._optionsService.options.fontWeight;
const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * window.devicePixelRatio}px ${this._optionsService.rawOptions.fontFamily}`;
}
private _getContrastColor(cell: CellData): IColor | undefined {
if (this._optionsService.options.minimumContrastRatio === 1) {
if (this._optionsService.rawOptions.minimumContrastRatio === 1) {
return undefined;
}
@@ -460,7 +455,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
if (!result) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
@@ -500,7 +495,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._colors.ansi[fgColor].rgba;
+6 -6
View File
@@ -94,7 +94,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onOptionsChanged(): void {
if (this._optionsService.options.cursorBlink) {
if (this._optionsService.rawOptions.cursorBlink) {
if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true);
@@ -148,7 +148,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
const cursorStyle = this._optionsService.options.cursorStyle;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
} else {
@@ -174,7 +174,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._state.x === cursorX &&
this._state.y === viewportRelativeCursorY &&
this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === this._optionsService.options.cursorStyle &&
this._state.style === this._optionsService.rawOptions.cursorStyle &&
this._state.width === this._cell.getWidth()) {
return;
}
@@ -182,13 +182,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
this._ctx.save();
this._cursorRenderers[this._optionsService.options.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
this._cursorRenderers[this._optionsService.rawOptions.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
this._ctx.restore();
this._state.x = cursorX;
this._state.y = viewportRelativeCursorY;
this._state.isFocused = false;
this._state.style = this._optionsService.options.cursorStyle;
this._state.style = this._optionsService.rawOptions.cursorStyle;
this._state.width = this._cell.getWidth();
}
@@ -213,7 +213,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth);
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
this._ctx.restore();
}
+8 -7
View File
@@ -10,10 +10,11 @@ import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
import { ICharSizeService } from 'browser/services/Services';
import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IDecorationOptions, IDecoration } from 'xterm';
let nextRendererId = 1;
@@ -39,7 +40,7 @@ export class Renderer extends Disposable implements IRenderer {
@IOptionsService private readonly _optionsService: IOptionsService
) {
super();
const allowTransparency = this._optionsService.options.allowTransparency;
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
this._renderLayers = [
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
@@ -178,18 +179,18 @@ export class Renderer extends Disposable implements IRenderer {
// will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than
// scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas
+3 -3
View File
@@ -225,7 +225,7 @@ export class TextRenderLayer extends BaseRenderLayer {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
@@ -237,7 +237,7 @@ export class TextRenderLayer extends BaseRenderLayer {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
} else {
let fg = cell.getFgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
fg += 8;
}
this._ctx.fillStyle = this._colors.ansi[fg].css;
@@ -271,7 +271,7 @@ export class TextRenderLayer extends BaseRenderLayer {
}
public onOptionsChanged(): void {
this._setTransparency(this._optionsService.options.allowTransparency);
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
}
/**
+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,
+3 -3
View File
@@ -3,13 +3,13 @@
* @license MIT
*/
import { isFirefox } from 'common/Platform';
import { isFirefox, isLegacyEdge } 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
// The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge 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 TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic';
export const CHAR_ATLAS_CELL_SPACING = 1;
+9 -9
View File
@@ -107,8 +107,8 @@ export class DomRenderer extends Disposable implements IRenderer {
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
@@ -161,16 +161,16 @@ export class DomRenderer extends Disposable implements IRenderer {
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this._colors.foreground.css};` +
` font-family: ${this._optionsService.options.fontFamily};` +
` font-size: ${this._optionsService.options.fontSize}px;` +
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
`}`;
// Text styles
styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
` font-weight: ${this._optionsService.options.fontWeight};` +
` font-weight: ${this._optionsService.rawOptions.fontWeight};` +
`}` +
`${this._terminalSelector} span.${BOLD_CLASS} {` +
` font-weight: ${this._optionsService.options.fontWeightBold};` +
` font-weight: ${this._optionsService.rawOptions.fontWeightBold};` +
`}` +
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
` font-style: italic;` +
@@ -210,7 +210,7 @@ export class DomRenderer extends Disposable implements IRenderer {
` color: ${this._colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: ${this._optionsService.options.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
@@ -356,7 +356,7 @@ export class DomRenderer extends Disposable implements IRenderer {
public renderRows(start: number, end: number): void {
const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
const cursorBlink = this._optionsService.options.cursorBlink;
const cursorBlink = this._optionsService.rawOptions.cursorBlink;
for (let y = start; y <= end; y++) {
const rowElement = this._rowElements[y];
@@ -364,7 +364,7 @@ export class DomRenderer extends Disposable implements IRenderer {
const row = y + this._bufferService.buffer.ydisp;
const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = this._optionsService.options.cursorStyle;
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
}
}
@@ -175,7 +175,7 @@ export class DomRendererRowFactory {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (cell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
fg += 8;
}
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
@@ -225,7 +225,7 @@ export class DomRendererRowFactory {
}
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
if (this._optionsService.options.minimumContrastRatio === 1) {
if (this._optionsService.rawOptions.minimumContrastRatio === 1) {
return false;
}
@@ -234,7 +234,7 @@ export class DomRendererRowFactory {
// Calculate and store in cache
if (adjustedColor === undefined) {
adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.rawOptions.minimumContrastRatio);
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
}
@@ -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]];
+2 -2
View File
@@ -69,8 +69,8 @@ class DomMeasureStrategy implements IMeasureStrategy {
}
public measure(): IReadonlyMeasureResult {
this._measureElement.style.fontFamily = this._optionsService.options.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.options.fontSize}px`;
this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily;
this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`;
// Note that this triggers a synchronous layout
const geometry = this._measureElement.getBoundingClientRect();
+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;
}
/**
+164
View File
@@ -0,0 +1,164 @@
/**
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDecorationService, IRenderService } from 'browser/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { Disposable } from 'common/Lifecycle';
import { IBufferService, IInstantiationService } from 'common/services/Services';
import { IDecorationOptions, IDecoration, IMarker } from 'xterm';
export class DecorationService extends Disposable implements IDecorationService {
private readonly _decorations: Decoration[] = [];
private _container: HTMLElement | undefined;
private _screenElement: HTMLElement | undefined;
private _renderService: IRenderService | undefined;
private _animationFrame: number | undefined;
constructor(@IInstantiationService private readonly _instantiationService: IInstantiationService) { super(); }
public attachToDom(screenElement: HTMLElement, renderService: IRenderService): void {
this._renderService = renderService;
this._screenElement = screenElement;
this._container = document.createElement('div');
this._container.classList.add('xterm-decoration-container');
screenElement.appendChild(this._container);
this.register(this._renderService.onRenderedBufferChange(() => this.refresh()));
this.register(this._renderService.onDimensionsChange(() => this.refresh(true)));
}
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined {
if (decorationOptions.marker.isDisposed || !this._container) {
return undefined;
}
const decoration = this._instantiationService.createInstance(Decoration, decorationOptions, this._container);
this._decorations.push(decoration);
decoration.onDispose(() => this._decorations.splice(this._decorations.indexOf(decoration), 1));
this._queueRefresh();
return decoration;
}
private _queueRefresh(): void {
if (this._animationFrame !== undefined) {
return;
}
this._animationFrame = window.requestAnimationFrame(() => {
this.refresh();
this._animationFrame = undefined;
});
}
public refresh(shouldRecreate?: boolean): void {
if (!this._renderService) {
return;
}
for (const decoration of this._decorations) {
decoration.render(this._renderService, shouldRecreate);
}
}
public dispose(): void {
for (const decoration of this._decorations) {
decoration.dispose();
}
if (this._screenElement && this._container && this._screenElement.contains(this._container)) {
this._screenElement.removeChild(this._container);
}
}
}
export class Decoration extends Disposable implements IDecoration {
private readonly _marker: IMarker;
private _element: HTMLElement | undefined;
public isDisposed: boolean = false;
public get element(): HTMLElement | undefined { return this._element; }
public get marker(): IMarker { return this._marker; }
private _onDispose = new EventEmitter<void>();
public get onDispose(): IEvent<void> { return this._onDispose.event; }
private _onRender = new EventEmitter<HTMLElement>();
public get onRender(): IEvent<HTMLElement> { return this._onRender.event; }
public x: number;
public anchor: 'left' | 'right';
public width: number;
public height: number;
constructor(
options: IDecorationOptions,
private readonly _container: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService
) {
super();
this.x = options.x ?? 0;
this._marker = options.marker;
this._marker.onDispose(() => this.dispose());
this.anchor = options.anchor || 'left';
this.width = options.width || 1;
this.height = options.height || 1;
}
public render(renderService: IRenderService, shouldRecreate?: boolean): void {
if (!this._element || shouldRecreate) {
this._createElement(renderService, shouldRecreate);
}
if (this._container && this._element && !this._container.contains(this._element)) {
this._container.append(this._element);
}
this._refreshStyle(renderService);
if (this._element) {
this._onRender.fire(this._element);
}
}
private _createElement(renderService: IRenderService, shouldRecreate?: boolean): void {
if (shouldRecreate && this._element && this._container.contains(this._element)) {
this._container.removeChild(this._element);
}
this._element = document.createElement('div');
this._element.classList.add('xterm-decoration');
this._element.style.width = `${this.width * renderService.dimensions.actualCellWidth}px`;
this._element.style.height = `${this.height * renderService.dimensions.actualCellHeight}px`;
this._element.style.top = `${(this.marker.line - this._bufferService.buffers.active.ydisp) * renderService.dimensions.actualCellHeight}px`;
this._element.style.lineHeight = `${renderService.dimensions.actualCellHeight}px`;
if (this.x && this.x > this._bufferService.cols) {
// exceeded the container width, so hide
this._element.style.display = 'none';
}
if (this.anchor === 'right') {
this._element.style.right = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : '';
} else {
this._element.style.left = this.x ? `${this.x * renderService.dimensions.actualCellWidth}px` : '';
}
}
private _refreshStyle(renderService: IRenderService): void {
if (!this._element) {
return;
}
const line = this.marker.line - this._bufferService.buffers.active.ydisp;
if (line < 0 || line > this._bufferService.rows) {
// outside of viewport
this._element.style.display = 'none';
} else {
this._element.style.top = `${line * renderService.dimensions.actualCellHeight}px`;
this._element.style.display = this._bufferService.buffer === this._bufferService.buffers.alt ? 'none' : 'block';
}
}
public override dispose(): void {
if (this.isDisposed) {
return;
}
if (this._element && this._container.contains(this._element)) {
this._container.removeChild(this._element);
}
this.isDisposed = true;
this._onDispose.fire();
}
}
+2 -1
View File
@@ -65,7 +65,8 @@ export class RenderService extends Disposable implements IRenderService {
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
this.register(this._screenDprMonitor);
this.register(bufferService.onResize(e => this._fullRefresh()));
this.register(bufferService.onResize(() => this._fullRefresh()));
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
+5 -5
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);
@@ -414,7 +414,7 @@ export class SelectionService extends Disposable implements ISelectionService {
*/
public shouldForceSelection(event: MouseEvent): boolean {
if (Browser.isMac) {
return event.altKey && this._optionsService.options.macOptionClickForcesSelection;
return event.altKey && this._optionsService.rawOptions.macOptionClickForcesSelection;
}
return event.shiftKey;
@@ -567,7 +567,7 @@ export class SelectionService extends Disposable implements ISelectionService {
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
return event.altKey && !(Browser.isMac && this._optionsService.options.macOptionClickForcesSelection);
return event.altKey && !(Browser.isMac && this._optionsService.rawOptions.macOptionClickForcesSelection);
}
/**
@@ -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;
@@ -993,7 +993,7 @@ export class SelectionService extends Disposable implements ISelectionService {
if (cell.getWidth() === 0) {
return false;
}
return this._optionsService.options.wordSeparator.indexOf(cell.getChars()) >= 0;
return this._optionsService.rawOptions.wordSeparator.indexOf(cell.getChars()) >= 0;
}
/**
+10
View File
@@ -9,6 +9,8 @@ import { IColorSet } from 'browser/Types';
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
import { IDisposable } from 'common/Types';
import { IDecorationOptions, IDecoration } from 'xterm';
import { IBufferService } from 'common/services/Services';
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
export interface ICharSizeService {
@@ -113,3 +115,11 @@ export interface ICharacterJoinerService {
deregister(joinerId: number): boolean;
getJoinedCharacters(row: number): [number, number][];
}
export const IDecorationService = createDecorator<IDecorationService>('DecorationService');
export interface IDecorationService extends IDisposable {
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
refresh(): void;
attachToDom(screenElement: HTMLElement, renderService: IRenderService, bufferService: IBufferService): void;
}
+1 -1
View File
@@ -34,7 +34,7 @@ export class SoundService implements ISoundService {
return;
}
const bellAudioSource = ctx.createBufferSource();
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.options.bellSound)), (buffer) => {
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.rawOptions.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(ctx.destination);
bellAudioSource.start(0);
+10 -5
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,7 +86,12 @@ 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.publicOptions; }
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: Partial<ITerminalOptions>
@@ -242,7 +247,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
}
protected _setup(): void {
if (this.optionsService.options.windowsMode) {
if (this.optionsService.rawOptions.windowsMode) {
this._enableWindowsMode();
}
}
@@ -262,7 +267,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.buffers.resize(this.cols, this.rows);
break;
case 'windowsMode':
if (this.optionsService.options.windowsMode) {
if (this.optionsService.rawOptions.windowsMode) {
this._enableWindowsMode();
} else {
this._windowsMode?.dispose();
+184 -49
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.
@@ -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 }]]);
});
});
+195 -59
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,9 +176,9 @@ 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 };
let style = STYLES[this._optionsService.options.cursorStyle];
style -= this._optionsService.options.cursorBlink ? 1 : 0;
const STYLES: { [key: string]: number } = { 'block': 2, 'underline': 4, 'bar': 6 };
let style = STYLES[this._optionsService.rawOptions.cursorStyle];
style -= this._optionsService.rawOptions.cursorBlink ? 1 : 0;
this._coreService.triggerDataEvent(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
break;
default:
@@ -262,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,
@@ -398,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.
@@ -417,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.
@@ -536,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}"` : ` "${Array.prototype.map.call(data, e => String.fromCharCode(e)).join('')}"`}`, typeof data === 'string'
? data.split('').map(e => e.charCodeAt(0))
: data
);
}
// resize input buffer if needed
if (this._parseBuffer.length < data.length) {
@@ -589,7 +603,7 @@ export class InputHandler extends Disposable implements IInputHandler {
let code: number;
let chWidth: number;
const charset = this._charsetService.charset;
const screenReaderMode = this._optionsService.options.screenReaderMode;
const screenReaderMode = this._optionsService.rawOptions.screenReaderMode;
const cols = this._bufferService.cols;
const wraparoundMode = this._coreService.decPrivateModes.wraparound;
const insertMode = this._coreService.modes.insertMode;
@@ -731,7 +745,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (id.final === 't' && !id.prefix && !id.intermediates) {
// security: always check whether window option is allowed
return this._parser.registerCsiHandler(id, params => {
if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) {
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
return true;
}
return callback(params);
@@ -786,7 +800,7 @@ export class InputHandler extends Disposable implements IInputHandler {
*/
public lineFeed(): boolean {
this._dirtyRowService.markDirty(this._activeBuffer.y);
if (this._optionsService.options.convertEol) {
if (this._optionsService.rawOptions.convertEol) {
this._activeBuffer.x = 0;
}
this._activeBuffer.y++;
@@ -855,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;
@@ -891,7 +904,7 @@ export class InputHandler extends Disposable implements IInputHandler {
}
const originalX = this._activeBuffer.x;
this._activeBuffer.x = this._activeBuffer.nextStop();
if (this._optionsService.options.screenReaderMode) {
if (this._optionsService.rawOptions.screenReaderMode) {
this._onA11yTab.fire(this._activeBuffer.x - originalX);
}
return true;
@@ -1195,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)!;
@@ -1217,6 +1231,7 @@ export class InputHandler extends Disposable implements IInputHandler {
private _resetBufferLine(y: number): void {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()));
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
line.isWrapped = false;
}
@@ -1320,13 +1335,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);
@@ -1749,7 +1764,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* @param term The terminal name to evaluate
*/
private _is(term: string): boolean {
return (this._optionsService.options.termName + '').indexOf(term) === 0;
return (this._optionsService.rawOptions.termName + '').indexOf(term) === 0;
}
/**
@@ -1915,7 +1930,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* This is only active if 'SetWinLines' (24) is enabled
* through `options.windowsOptions`.
*/
if (this._optionsService.options.windowOptions.setWinLines) {
if (this._optionsService.rawOptions.windowOptions.setWinLines) {
this._bufferService.resize(132, this._bufferService.rows);
this._onRequestReset.fire();
}
@@ -1977,7 +1992,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());
@@ -2149,7 +2164,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* This is only active if 'SetWinLines' (24) is enabled
* through `options.windowsOptions`.
*/
if (this._optionsService.options.windowOptions.setWinLines) {
if (this._optionsService.rawOptions.windowOptions.setWinLines) {
this._bufferService.resize(80, this._bufferService.rows);
this._onRequestReset.fire();
}
@@ -2197,7 +2212,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
@@ -2264,7 +2279,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
@@ -2683,7 +2698,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;
}
@@ -2726,7 +2741,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* Ps >= 24 not implemented
*/
public windowOptions(params: IParams): boolean {
if (!paramToWindowOption(params.params[0], this._optionsService.options.windowOptions)) {
if (!paramToWindowOption(params.params[0], this._optionsService.rawOptions.windowOptions)) {
return true;
}
const second = (params.length > 1) ? params.params[1] : 0;
@@ -2840,46 +2855,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
+1
View File
@@ -18,6 +18,7 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = userAgent.includes('Firefox');
export const isLegacyEdge = userAgent.includes('Edge');
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
// Find the users platform. We use this to interpret the meta key
+8 -4
View File
@@ -120,17 +120,21 @@ export class MockLogService implements ILogService {
export class MockOptionsService implements IOptionsService {
public serviceBrand: any;
public options: ITerminalOptions = clone(DEFAULT_OPTIONS);
public publicOptions: ITerminalOptions = clone(DEFAULT_OPTIONS);
public readonly rawOptions: ITerminalOptions = clone(DEFAULT_OPTIONS);
public options: ITerminalOptions = this.rawOptions;
public onOptionChange: IEvent<string> = new EventEmitter<string>().event;
constructor(testOptions?: Partial<ITerminalOptions>) {
if (testOptions) {
for (const key of Object.keys(testOptions)) {
this.options[key] = testOptions[key];
this.publicOptions[key] = testOptions[key];
this.rawOptions[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.');
}
+95 -72
View File
@@ -46,6 +46,7 @@ export interface IKeyboardEvent {
ctrlKey: boolean;
shiftKey: boolean;
metaKey: boolean;
/** @deprecated See KeyboardEvent.keyCode */
keyCode: number;
key: string;
type: string;
@@ -348,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.
@@ -375,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 {
+31 -5
View File
@@ -43,6 +43,7 @@ export class Buffer implements IBuffer {
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
private _cols: number;
private _rows: number;
private _isClearing: boolean = false;
constructor(
private _hasScrollback: boolean,
@@ -107,7 +108,7 @@ export class Buffer implements IBuffer {
return rows;
}
const correctBufferLength = rows + this._optionsService.options.scrollback;
const correctBufferLength = rows + this._optionsService.rawOptions.scrollback;
return correctBufferLength > MAX_BUFFER_SIZE ? MAX_BUFFER_SIZE : correctBufferLength;
}
@@ -172,7 +173,7 @@ export class Buffer implements IBuffer {
if (this._rows < newRows) {
for (let y = this._rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) {
if (this._optionsService.options.windowsMode) {
if (this._optionsService.rawOptions.windowsMode) {
// Just add the new missing rows on Windows as conpty reprints the screen with it's
// view of the world. Once a line enters scrollback for conpty it remains there
this.lines.push(new BufferLine(newCols, nullCell));
@@ -252,7 +253,7 @@ export class Buffer implements IBuffer {
}
private get _isReflowEnabled(): boolean {
return this._hasScrollback && !this._optionsService.options.windowsMode;
return this._hasScrollback && !this._optionsService.rawOptions.windowsMode;
}
private _reflow(newCols: number, newRows: number): void {
@@ -367,6 +368,11 @@ export class Buffer implements IBuffer {
let srcCol = lastLineLength;
while (srcLineIndex >= 0) {
const cellsToCopy = Math.min(srcCol, destCol);
if (wrappedLines[destLineIndex] === undefined) {
// Sanity check that the line exists, this has been known to fail for an unknown reason
// which would stop the reflow from happening if an exception would throw.
break;
}
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
destCol -= cellsToCopy;
if (destCol === 0) {
@@ -550,7 +556,7 @@ export class Buffer implements IBuffer {
i = 0;
}
for (; i < this._cols; i += this._optionsService.options.tabStopWidth) {
for (; i < this._cols; i += this._optionsService.rawOptions.tabStopWidth) {
this.tabs[i] = true;
}
}
@@ -579,6 +585,24 @@ export class Buffer implements IBuffer {
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
}
public clearMarkers(y?: number): void {
this._isClearing = true;
if (y !== undefined) {
for (let i = 0; i < this.markers.length; i++) {
if (this.markers[i].line === y) {
this.markers[i].dispose();
this.markers.splice(i--, 1);
}
}
} else {
for (const marker of this.markers) {
marker.dispose();
}
this.markers = [];
}
this._isClearing = false;
}
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
@@ -610,7 +634,9 @@ export class Buffer implements IBuffer {
}
private _removeMarker(marker: Marker): void {
this.markers.splice(this.markers.indexOf(marker), 1);
if (!this._isClearing) {
this.markers.splice(this.markers.indexOf(marker), 1);
}
}
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
+1
View File
@@ -45,6 +45,7 @@ export interface IBuffer {
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
addMarker(y: number): IMarker;
clearMarkers(y?: number): void;
}
export interface IBufferSet extends IDisposable {
+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]);
}
+2 -2
View File
@@ -36,8 +36,8 @@ export class BufferService extends Disposable implements IBufferService {
@IOptionsService private _optionsService: IOptionsService
) {
super();
this.cols = Math.max(_optionsService.options.cols || 0, MINIMUM_COLS);
this.rows = Math.max(_optionsService.options.rows || 0, MINIMUM_ROWS);
this.cols = Math.max(_optionsService.rawOptions.cols || 0, MINIMUM_COLS);
this.rows = Math.max(_optionsService.rawOptions.rows || 0, MINIMUM_ROWS);
this.buffers = new BufferSet(_optionsService, this);
}
+2 -2
View File
@@ -62,7 +62,7 @@ export class CoreService extends Disposable implements ICoreService {
public triggerDataEvent(data: string, wasUserInput: boolean = false): void {
// Prevents all events to pty process if stdin is disabled
if (this._optionsService.options.disableStdin) {
if (this._optionsService.rawOptions.disableStdin) {
return;
}
@@ -83,7 +83,7 @@ export class CoreService extends Disposable implements ICoreService {
}
public triggerBinaryEvent(data: string): void {
if (this._optionsService.options.disableStdin) {
if (this._optionsService.rawOptions.disableStdin) {
return;
}
this._logService.debug(`sending binary "${data}"`, () => data.split('').map(e => e.charCodeAt(0)));
+1 -1
View File
@@ -46,7 +46,7 @@ export class LogService implements ILogService {
}
private _updateLogLevel(): void {
this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
this.logLevel = optionsKeyToLogLevel[this._optionsService.rawOptions.logLevel];
}
private _evalLazyOptionalParams(optionalParams: any[]): void {
+6 -2
View File
@@ -10,7 +10,7 @@ describe('OptionsService', () => {
describe('constructor', () => {
const originalError = console.error;
beforeEach(() => {
console.error = () => {};
console.error = () => { };
});
afterEach(() => {
console.error = originalError;
@@ -26,7 +26,11 @@ describe('OptionsService', () => {
assert.equal(optionsService.getOption('cols'), 80);
});
it('uses default value if invalid constructor option value passed', () => {
assert.equal(new OptionsService({tabStopWidth: 0}).getOption('tabStopWidth'), DEFAULT_OPTIONS.tabStopWidth);
assert.equal(new OptionsService({ tabStopWidth: 0 }).getOption('tabStopWidth'), DEFAULT_OPTIONS.tabStopWidth);
});
it('object.keys return the correct number of options', () => {
const optionsService = new OptionsService({ cols: 80, rows: 25 });
assert.notEqual(Object.keys(optionsService.options).length, 0);
});
});
describe('setOption', () => {
+34 -43
View File
@@ -57,29 +57,23 @@ export const DEFAULT_OPTIONS: Readonly<ITerminalOptions> = {
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 readonly rawOptions: ITerminalOptions;
public options: ITerminalOptions;
public publicOptions: ITerminalOptions;
private _onOptionChange = new EventEmitter<string>();
public get onOptionChange(): IEvent<string> { return this._onOptionChange.event; }
constructor(options: Partial<ITerminalOptions>) {
// set the default value of each option
this._options = { ...DEFAULT_OPTIONS };
const defaultOptions = { ...DEFAULT_OPTIONS };
for (const key in options) {
if (key in this._options) {
if (key in defaultOptions) {
try {
const newValue = options[key];
this._options[key] = this._sanitizeAndValidateOption(key, newValue);
defaultOptions[key] = this._sanitizeAndValidateOption(key, newValue);
} catch (e) {
console.error(e);
}
@@ -87,46 +81,43 @@ export class OptionsService implements IOptionsService {
}
// set up getters and setters for each option
this.options = this._setupOptions(this._options, false);
this.publicOptions = this._setupOptions(this._options, true);
this.rawOptions = defaultOptions;
this.options = { ... defaultOptions };
this._setupOptions();
}
private _setupOptions(options: ITerminalOptions, isPublic: boolean): 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}"`);
}
private _setupOptions(): void {
const getter = (propName: string): any => {
if (!(propName in DEFAULT_OPTIONS)) {
throw new Error(`No option with key "${propName}"`);
}
return this.rawOptions[propName];
};
// Throw an error if any constructor only option is modified
// from terminal.options
// Modifications from anywhere else are allowed
if (isPublic && CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {
throw new Error(`Option "${propName}" can only be set in the constructor`);
}
const setter = (propName: string, value: any): void => {
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);
}
}
});
value = this._sanitizeAndValidateOption(propName, value);
// Don't fire an option change event if they didn't change
if (this.rawOptions[propName] !== value) {
this.rawOptions[propName] = value;
this._onOptionChange.fire(propName);
}
};
for (const propName in this.rawOptions) {
const desc = {
get: getter.bind(this, propName),
set: setter.bind(this, propName)
};
Object.defineProperty(this.options, propName, desc);
}
return copiedOptions;
}
public setOption(key: string, value: any): void {
this.publicOptions[key] = value;
this.options[key] = value;
}
private _sanitizeAndValidateOption(key: string, value: any): any {
@@ -181,6 +172,6 @@ export class OptionsService implements IOptionsService {
}
public getOption(key: string): any {
return this.publicOptions[key];
return this.options[key];
}
}
+16 -9
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;
@@ -180,8 +188,13 @@ export const IOptionsService = createDecorator<IOptionsService>('OptionsService'
export interface IOptionsService {
serviceBrand: undefined;
/**
* Read only access to the raw options object, this is an internal-only fast path for accessing
* single options without any validation as we trust TypeScript to enforce correct usage
* internally.
*/
readonly rawOptions: Readonly<ITerminalOptions>;
readonly options: ITerminalOptions;
readonly publicOptions: ITerminalOptions;
readonly onOptionChange: IEvent<string>;
@@ -191,13 +204,7 @@ 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 ITerminalOptions {
@@ -207,6 +214,7 @@ export interface ITerminalOptions {
bellSound: string;
bellStyle: 'none' | 'sound' /* | 'visual' | 'both' */;
cols: number;
convertEol: boolean;
cursorBlink: boolean;
cursorStyle: 'block' | 'underline' | 'bar';
cursorWidth: number;
@@ -240,7 +248,6 @@ export interface ITerminalOptions {
[key: string]: any;
cancelEvents: boolean;
convertEol: boolean;
termName: string;
}
+35 -4
View File
@@ -5,10 +5,11 @@
import { deepStrictEqual, strictEqual, throws } from 'assert';
import { Terminal } from 'headless/public/Terminal';
import { ITerminalOptions } from 'xterm-headless';
let term: Terminal;
describe('Headless API Tests', function(): void {
describe('Headless API Tests', function (): void {
beforeEach(() => {
// Create default terminal to be used by most tests
term = new Terminal();
@@ -119,13 +120,43 @@ describe('Headless API Tests', function(): void {
strictEqual(term.getOption('scrollback'), 50);
});
describe('options', () => {
const termOptions = {
cols: 80,
rows: 24
};
beforeEach(async () => {
term = new Terminal(termOptions);
});
it('get options', () => {
const options: ITerminalOptions = term.options;
strictEqual(options.cols, 80);
strictEqual(options.rows, 24);
});
it('set options', async () => {
const options: ITerminalOptions = term.options;
throws(() => options.cols = 40);
throws(() => options.rows = 20);
term.options.scrollback = 1;
strictEqual(term.options.scrollback, 1);
term.options= {
fontSize: 12,
fontFamily: 'Arial'
};
strictEqual(term.options.fontSize, 12);
strictEqual(term.options.fontFamily, 'Arial');
});
});
describe('loadAddon', () => {
it('constructor', async () => {
term = new Terminal({ cols: 5 });
let cols = 0;
term.loadAddon({
activate: (t) => cols = t.cols,
dispose: () => {}
dispose: () => { }
});
strictEqual(cols, 5);
});
@@ -133,7 +164,7 @@ describe('Headless API Tests', function(): void {
it('dispose (addon)', async () => {
let disposeCalled = false;
const addon = {
activate: () => {},
activate: () => { },
dispose: () => disposeCalled = true
};
term.loadAddon(addon);
@@ -145,7 +176,7 @@ describe('Headless API Tests', function(): void {
it('dispose (terminal)', async () => {
let disposeCalled = false;
term.loadAddon({
activate: () => {},
activate: () => { },
dispose: () => disposeCalled = true
});
strictEqual(disposeCalled, false);
+52 -2
View File
@@ -7,19 +7,61 @@ import { IEvent } from 'common/EventEmitter';
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
import { ParserApi } from 'common/public/ParserApi';
import { UnicodeApi } from 'common/public/UnicodeApi';
import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, ITerminalOptions, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless';
import { IBufferNamespace as IBufferNamespaceApi, IMarker, IModes, IParser, ITerminalAddon, IUnicodeHandling, Terminal as ITerminalApi } from 'xterm-headless';
import { Terminal as TerminalCore } from 'headless/Terminal';
import { AddonManager } from 'common/public/AddonManager';
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: TerminalCore;
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 = { ... this._core.options };
const getter = (propName: string): any => {
return this._core.options[propName];
};
const setter = (propName: string, value: any): void => {
this._checkReadonlyOptions(propName);
this._core.options[propName] = value;
};
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;
}
});
const desc = {
get: getter.bind(this, propName),
set: setter.bind(this, propName)
};
Object.defineProperty(this._publicOptions, propName, desc);
}
}
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 {
@@ -82,11 +124,19 @@ 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 resize(columns: number, rows: number): void {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
}
public registerMarker(cursorYOffset: number): IMarker | undefined {
public registerMarker(cursorYOffset: number = 0): IMarker | undefined {
this._checkProposedApi();
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
+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 () => {
+104
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,40 @@ 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');
});
it('object.keys return the correct number of options', async () => {
await openTerminal(page);
assert.notEqual(await page.evaluate(`Object.keys(window.term.options).length`), 0);
});
});
describe('renderer', () => {
it('foreground', async () => {
await openTerminal(page, { rendererType: 'dom' });
@@ -524,6 +559,29 @@ describe('API Integration Tests', function(): void {
assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getChars()`), '');
assert.equal(await page.evaluate(`window.term.buffer.active.getLine(0).getCell(2).getWidth()`), 0);
});
it('clearMarkers', async () => {
await openTerminal(page, { cols: 5 });
await page.evaluate(`
window.disposeStack = [];
`);
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await page.evaluate(`window.term.addMarker(1)`);
await page.evaluate(`window.term.addMarker(2)`);
await page.evaluate(`window.term.scrollLines(10)`);
await page.evaluate(`window.term.addMarker(3)`);
await page.evaluate(`window.term.addMarker(4)`);
await page.evaluate(`
for (let i = 0; i < window.term.markers.length; ++i) {
const marker = window.term.markers[i];
marker.onDispose(() => window.disposeStack.push(marker));
}`);
await page.evaluate(`window.term.clear()`);
assert.equal(await page.evaluate(`window.disposeStack.length`), 4);
});
});
it('active, normal, alternate', async () => {
@@ -673,6 +731,52 @@ describe('API Integration Tests', function(): void {
await pollFor(page, `window.term._core._renderService.dimensions.actualCellWidth > 0`, true);
});
describe('registerDecoration', () => {
it('should register decorations and render them', async () => {
await openTerminal(page);
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await page.evaluate(`window.marker1 = window.term.addMarker(1)`);
await page.evaluate(`window.marker2 = window.term.addMarker(2)`);
await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`);
await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`);
await page.evaluate(`window.term.resize(10, 5)`);
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 2);
});
it('on resize should dispose of the old decoration and create a new one', async () => {
await openTerminal(page);
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await page.evaluate(`window.marker = window.term.addMarker(1)`);
await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker })`);
await page.evaluate(`window.term.resize(10, 5)`);
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm-screen .xterm-decoration').length`), 1);
});
it('should return undefined when the marker has already been disposed of', async () => {
await openTerminal(page);
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await page.evaluate(`window.marker = window.term.addMarker(1)`);
await page.evaluate(`window.marker.dispose()`);
assert.equal(await page.evaluate(`window.decoration = window.term.registerDecoration({ marker: window.marker });`), undefined);
});
it('should throw when a negative x offset is provided', async () => {
await openTerminal(page);
await writeSync(page, '\\n\\n\\n\\n');
await writeSync(page, '\\n\\n\\n\\n');
await page.evaluate(`window.marker = window.term.addMarker(1)`);
await page.evaluate(`
try {
window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 });
} catch (e) {
window.throwMessage = e.message;
}
`);
await pollFor(page, 'window.throwMessage', 'This API only accepts positive integers');
});
});
describe('registerLinkProvider', () => {
it('should fire provideLinks when hovering cells', 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"
+24 -1
View File
@@ -534,6 +534,29 @@ declare module 'xterm-headless' {
*/
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.
*/
@@ -620,7 +643,7 @@ declare module 'xterm-headless' {
* @param cursorYOffset The y position offset of the marker from the cursor.
* @returns The new marker or undefined.
*/
registerMarker(cursorYOffset: number): IMarker | undefined;
registerMarker(cursorYOffset?: number): IMarker | undefined;
/**
* @deprecated use `registerMarker` instead.
+111 -17
View File
@@ -380,29 +380,96 @@ declare module 'xterm' {
* is trimmed and lines are added or removed. This is a single line that may
* be part of a larger wrapped line.
*/
export interface IMarker extends IDisposable {
export interface IMarker extends IDisposableWithEvent {
/**
* A unique identifier for this marker.
*/
readonly id: number;
/**
* Whether this marker is disposed.
*/
readonly isDisposed: boolean;
/**
* The actual line index in the buffer at this point in time. This is set to
* -1 if the marker has been disposed.
*/
readonly line: number;
}
/**
* Represents a disposable with an
* @param onDispose event listener and
* @param isDisposed property.
*/
export interface IDisposableWithEvent extends IDisposable {
/**
* Event listener to get notified when the marker gets disposed. Automatic disposal
* might happen for a marker, that got invalidated by scrolling out or removal of
* a line from the buffer.
* Event listener to get notified when this gets disposed.
*/
onDispose: IEvent<void>;
/**
* Whether this is disposed.
*/
readonly isDisposed: boolean;
}
/**
* Represents a decoration in the terminal that is associated with a particular marker and DOM element.
*/
export interface IDecoration extends IDisposableWithEvent {
/*
* The marker for the decoration in the terminal.
*/
readonly marker: IMarker;
/**
* An event fired when the decoration
* is rendered, returns the dom element
* associated with the decoration.
*/
readonly onRender: IEvent<HTMLElement>;
/**
* The HTMLElement that gets created after the
* first _onRender call, or undefined if accessed before
* that.
*/
readonly element: HTMLElement | undefined;
}
/**
* Options provided when registering a decoration
* containing a @param marker, @param anchor,
* @param x offset from the anchor, @param width in cells
* and @param height in cells.
*/
export interface IDecorationOptions {
/**
* The line in the terminal where
* the decoration will be displayed
*/
marker: IMarker;
/*
* Where the decoration will be anchored -
* defaults to the left edge
*/
anchor?: 'right' | 'left';
/**
* The x position offset relative to the anchor
*/
x?: number;
/**
* The width of the decoration in cells, which defaults to
* cell width
*/
width?: number;
/**
* The height of the decoration in cells, which defaults to
* cell height
*/
height?: number;
}
/**
@@ -636,9 +703,27 @@ declare module 'xterm' {
readonly modes: IModes;
/**
* Get the terminal options
* 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',
* };
* ```
*/
readonly options: ITerminalOptions;
options: ITerminalOptions;
/**
* Natural language strings that can be localized.
@@ -794,9 +879,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;
@@ -845,13 +930,22 @@ declare module 'xterm' {
* @param cursorYOffset The y position offset of the marker from the cursor.
* @returns The new marker or undefined.
*/
registerMarker(cursorYOffset: number): IMarker | undefined;
registerMarker(cursorYOffset?: number): IMarker | undefined;
/**
* @deprecated use `registerMarker` instead.
*/
addMarker(cursorYOffset: number): IMarker | undefined;
/**
* (EXPERIMENTAL) Adds a decoration to the terminal using
* @param decorationOptions, which takes a marker and an optional anchor,
* width, height, and x offset from the anchor. Returns the decoration or
* undefined if the alt buffer is active or the marker has already been disposed of.
* @throws when options include a negative x offset.
*/
registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined;
/**
* Gets whether the terminal has an active selection.
*/
@@ -1265,12 +1359,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;
}
+37 -30
View File
@@ -1153,10 +1153,10 @@ chokidar@3.5.1:
optionalDependencies:
fsevents "~2.3.1"
chokidar@3.5.2:
version "3.5.2"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
chokidar@3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==
dependencies:
anymatch "~3.1.2"
braces "~3.0.2"
@@ -1398,7 +1398,14 @@ debug@4.3.1:
dependencies:
ms "2.1.2"
debug@4.3.2, debug@^4.3.1, debug@^4.3.2:
debug@4.3.3:
version "4.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
dependencies:
ms "2.1.2"
debug@^4.3.1, debug@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
@@ -2140,10 +2147,10 @@ glob@7.1.6, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@7.1.7, glob@^7.1.1:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
glob@7.2.0, glob@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -2152,10 +2159,10 @@ glob@7.1.7, glob@^7.1.1:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023"
integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==
glob@^7.1.1:
version "7.1.7"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90"
integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
@@ -2983,32 +2990,32 @@ mocha@^8.3.2:
yargs-parser "20.2.4"
yargs-unparser "2.0.0"
mocha@^9.1.3:
version "9.1.3"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.3.tgz#8a623be6b323810493d8c8f6f7667440fa469fdb"
integrity sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==
mocha@^9.2.0:
version "9.2.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.0.tgz#2bfba73d46e392901f877ab9a47b7c9c5d0275cc"
integrity sha512-kNn7E8g2SzVcq0a77dkphPsDSN7P+iYkqE0ZsGCYWRsoiKjOt+NvXfaagik8vuDa6W5Zw3qxe8Jfpt5qKf+6/Q==
dependencies:
"@ungap/promise-all-settled" "1.1.2"
ansi-colors "4.1.1"
browser-stdout "1.3.1"
chokidar "3.5.2"
debug "4.3.2"
chokidar "3.5.3"
debug "4.3.3"
diff "5.0.0"
escape-string-regexp "4.0.0"
find-up "5.0.0"
glob "7.1.7"
glob "7.2.0"
growl "1.10.5"
he "1.2.0"
js-yaml "4.1.0"
log-symbols "4.1.0"
minimatch "3.0.4"
ms "2.1.3"
nanoid "3.1.25"
nanoid "3.2.0"
serialize-javascript "6.0.0"
strip-json-comments "3.1.1"
supports-color "8.1.1"
which "2.0.2"
workerpool "6.1.5"
workerpool "6.2.0"
yargs "16.2.0"
yargs-parser "20.2.4"
yargs-unparser "2.0.0"
@@ -3048,10 +3055,10 @@ nanoid@3.1.20:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
nanoid@3.1.25:
version "3.1.25"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
nanoid@3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c"
integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA==
natural-compare@^1.4.0:
version "1.4.0"
@@ -4421,10 +4428,10 @@ workerpool@6.1.0:
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.0.tgz#a8e038b4c94569596852de7a8ea4228eefdeb37b"
integrity sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg==
workerpool@6.1.5:
version "6.1.5"
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581"
integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==
workerpool@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
wrap-ansi@^6.2.0:
version "6.2.0"