Compare commits

..
265 Commits
Author SHA1 Message Date
Daniel Imms a840db3e79 Merge pull request #2692 from xtermjs/Tyriar-patch-1
v4.4.0
2020-02-04 09:54:10 -08:00
Daniel Imms dc19d7c72c v4.4.0 2020-02-04 09:53:56 -08:00
Daniel Imms 4d0fc16fe3 Merge pull request #2691 from Tyriar/bump2
Bump serialize and unciode11 addon versions to trigger publish
2020-02-04 09:39:47 -08:00
Daniel Imms b13c3be92f Bump serialize and unciode11 addon versions to trigger publish 2020-02-04 09:39:18 -08:00
Daniel Imms a3a222c1f1 Merge pull request #2690 from Tyriar/version_bump_publish
Bump attach and webgl addons and automatically publish serialize and unicode11 addons
2020-02-04 09:29:04 -08:00
Daniel Imms 1d7cfcdd99 Bump webgl and attach versions 2020-02-04 09:19:49 -08:00
Daniel Imms 2c05e721fd Publish serialize and unicode11 addons automatically 2020-02-04 09:19:28 -08:00
Daniel Imms 22acf1ec25 Merge pull request #2618 from xtermjs/feat/serialize-addon
Merge serialize addon into master
2020-02-04 08:54:41 -08:00
Daniel Imms 9c8deb1470 Revert "Remove getNullCell"
This reverts commit 0b6a5b7630.
2020-02-04 08:42:24 -08:00
Daniel Imms 9b9cb5c076 Merge branch 'master' into feat/serialize-addon 2020-02-04 08:33:11 -08:00
Daniel Imms 0b6a5b7630 Remove getNullCell
Replace with fetching cell (0,0) and using that as the initial null cell. We
can reconsider adding this back if it's needed
2020-02-04 08:31:06 -08:00
Daniel Imms fac025367b Document IBufferCell members 2020-02-04 08:15:21 -08:00
Daniel Imms e5246f3993 jsdoc IBufferLine.length 2020-02-04 07:55:26 -08:00
Daniel Imms 89370a1f74 Explain why reusing IBufferCell is useful 2020-02-04 07:54:11 -08:00
Daniel Imms 51a564eb9c Remove links to serialize from test/benchmark 2020-02-04 07:45:12 -08:00
Daniel Imms b50b6ccb8b Fix fg used by selection in webgl again
See #2650
2020-02-04 07:41:04 -08:00
Daniel Imms 7a8e2c67b0 Merge pull request #2393 from jerch/window_manipulation
Support for window options
2020-02-04 07:38:51 -08:00
Daniel Imms 1b4a3475e8 Fix demo after bad merge 2020-02-04 07:07:34 -08:00
Daniel Imms 3d2d35f5b6 Pull render service off instantiation service safely 2020-02-04 07:01:25 -08:00
Daniel Imms 489d8110c9 Move windowOptions into public API part 2020-02-04 06:53:17 -08:00
Daniel Imms fc5bf99715 Merge branch 'master' into window_manipulation 2020-02-04 06:52:24 -08:00
Daniel Imms 999bdc9498 Merge branch 'master' into feat/serialize-addon 2020-02-04 06:52:00 -08:00
Daniel Imms 06e3528bc8 Merge pull request #2689 from Tyriar/2607_parser_stable
Make parser stable, remove deprecated APIs
2020-02-04 06:51:38 -08:00
Daniel Imms b06a88b09f Merge pull request #2688 from Tyriar/2608_safe_links
Handle links safely by removing opener from window
2020-02-04 06:51:24 -08:00
Daniel Imms 51aab3eacc Merge branch 'master' into window_manipulation 2020-02-04 06:51:13 -08:00
Daniel Imms 545807755d Merge pull request #2386 from jerch/extract_vt_features
document VT features in docstrings
2020-02-04 06:50:07 -08:00
Daniel Imms b7ae55a07b Make parser stable, remove deprecated APIs
Fixes #2607
2020-02-04 06:46:25 -08:00
Daniel Imms a980b3d132 Handle links safely by removing opener from window
Fixes #2608
2020-02-04 06:43:17 -08:00
Daniel Imms 58047e8b82 Merge branch 'master' into window_manipulation 2020-02-04 06:33:54 -08:00
Daniel Imms 9abeaa74c6 Merge pull request #2687 from JavaCS3/feat/serialize-addon
fix: api test fail due to IBufferCell api changes
2020-02-04 06:18:53 -08:00
javacs3 c512e48c52 fix: api test fail due to IBufferCell api changes 2020-02-04 14:46:03 +08:00
Daniel Imms 8924437fc5 Merge branch 'master' into feat/serialize-addon 2020-02-03 12:00:20 -08:00
Daniel Imms 1d8e242b3c Merge pull request #2369 from JavaCS3/feat/serialize-addon
Add foreground/background color support for SerializeAddon
2020-02-03 11:58:44 -08:00
Daniel Imms cf33f7c281 Merge branch 'feat/serialize-addon' into feat/serialize-addon 2020-02-03 11:54:11 -08:00
Daniel Imms 56ac6f66bf Merge pull request #2685 from Tyriar/2675_remove_options
Remove old unused options from codebase
2020-02-03 11:17:39 -08:00
jerch 5ac649b21a Merge branch 'master' into extract_vt_features 2020-02-03 20:06:33 +01:00
Daniel Imms 4a567c9f5a Remove old unused options from codebase
Fixes #2675
2020-02-03 10:56:46 -08:00
Daniel Imms 479fddfeec Merge branch 'master' into window_manipulation 2020-02-03 10:48:38 -08:00
Daniel Imms cc6f9e9da4 Merge pull request #2684 from Tyriar/2683_cursor_refresh
Refresh cursor row when cursor settings updated
2020-02-03 10:34:46 -08:00
Daniel Imms de9df12ea9 Merge remote-tracking branch 'ups/master' into pr/jerch/2393 2020-02-03 10:34:26 -08:00
Daniel Imms 8145829619 Merge pull request #2568 from jerch/updated_wcwidth
wcwidth rules for unicode 11
2020-02-03 10:27:47 -08:00
Daniel Imms 3bd0cb9a15 Merge branch 'master' into updated_wcwidth 2020-02-03 10:22:29 -08:00
Daniel Imms 412d6fe42c Refresh cursor row when cursor settings updated
Fixes #2683
2020-02-03 10:13:13 -08:00
Daniel Imms 12362cac61 Merge pull request #2679 from jerch/fix_2678
Add/fix chars in DEC graphics charset
2020-02-03 07:15:47 -08:00
Daniel Imms 001d26d73c Merge pull request #2670 from kumaran-14/patch-alt-click
Fixes Alt + click behaviour when rows are wrapped
2020-02-03 07:07:59 -08:00
Daniel Imms 75191982da Merge pull request #2682 from Tyriar/2681_exit_code_ci
Return exit code in Linux unit tests
2020-02-03 07:06:47 -08:00
Daniel Imms 8093a5f419 Fix lint 2020-02-03 07:02:14 -08:00
Daniel Imms 3d53847f30 Return exit code in Linux unit tests
Fixes #2681
2020-02-03 07:00:06 -08:00
Daniel Imms bb944f190e Fix and add more MoveToCell tests 2020-02-03 06:54:35 -08:00
Daniel Imms 7d84083f55 Fix algorithm to move the right amount of cells 2020-02-03 06:43:20 -08:00
kumaran-14 b591ccc40d Fix moveToCell when target is beyond buffer 2020-02-02 23:28:24 +05:30
Daniel Imms 46b95ed639 Merge pull request #2676 from jeffbaier/master
Add _terminalClass identifier to cursor blink animation css
2020-02-02 05:21:03 -08:00
Daniel Imms c050cf305c Merge pull request #2680 from Tyriar/2665_word_ep
Remove ; and : from default wordSeparator
2020-02-02 05:15:13 -08:00
Daniel Imms d5bf8966d9 Remove ; and : from default wordSeparator
Fixes #2665
2020-02-02 05:10:57 -08:00
Jörg Breitbart 49bc27e24f fixing visual repr chars 2020-02-02 13:07:11 +01:00
Jeff Baier 6240a5d7a4 Add _terminalClass identifier to cursor blink animation css
so that multiple DOM rendered instances no longer lose the cursor theme color
Fixes #2674
2020-01-22 14:16:39 -06:00
kumaran-14 af67b72492 Fix unit tests for MoveToCellSequence 2020-01-15 21:32:04 +05:30
Jörg Breitbart d2145368fa several template updates 2020-01-10 18:51:27 +01:00
Jörg Breitbart 4010900e47 simply status column, reason in title 2020-01-09 22:40:08 +01:00
Jörg Breitbart 2d1af70e87 lower header level; fix wrongly escaped sequences 2020-01-09 20:40:18 +01:00
kumaran-14 76d4de225a Run eslint and fix errors 2020-01-10 00:40:49 +05:30
kumaran-14 f5c3b00c42 Fixes Alt + click behaviour when rows are wrapped 2020-01-10 00:02:18 +05:30
Jörg Breitbart f9fddfc43d fix table sorting 2020-01-09 18:47:56 +01:00
Jörg Breitbart 12b912140b use 2 empty lines as end of vt definition 2020-01-09 16:40:35 +01:00
javacs3 56f4c3d6ae remove BufferCellApiView 2020-01-09 21:15:30 +08:00
jerch f560d187c1 Merge branch 'master' into window_manipulation 2020-01-09 00:27:22 +01:00
jerch d4d7446789 Merge branch 'master' into updated_wcwidth 2020-01-09 00:26:48 +01:00
jerch 663da772e0 Merge branch 'master' into extract_vt_features 2020-01-09 00:07:44 +01:00
Jörg Breitbart b51226f76c docs for SM/RM/DECSET/DECRST/SGR 2020-01-09 00:00:19 +01:00
Jörg Breitbart 297ea529a2 make linter happy 2020-01-08 18:08:39 +01:00
Jörg Breitbart 016d176cbd more docs 2020-01-08 17:51:56 +01:00
Daniel Imms 83e582b7ab Merge remote-tracking branch 'origin/master' into feat/serialize-addon 2020-01-07 10:30:21 -08:00
Daniel Imms fbeb45d1da Merge pull request #2667 from Tyriar/2666_windows_cup
Flag lines as wrapped after CUP in windows mode
2020-01-07 09:20:58 -08:00
Daniel Imms be24cdb917 Merge branch 'master' into 2666_windows_cup 2020-01-07 09:16:25 -08:00
Daniel Imms 685d14c4d5 Improve windows mode method name 2020-01-07 09:16:16 -08:00
Daniel Imms c50cd4cc3e Flag lines as wrapped after CUP in windows mode
Fixes #2666
2020-01-07 09:15:04 -08:00
Jörg Breitbart e240160d39 break command definition on new @vt entry 2020-01-07 01:01:46 +01:00
Jörg Breitbart dae8c3599b make linter happy 2020-01-07 00:47:07 +01:00
Jörg Breitbart 7d8c0268ea remove unicodeVersion from options 2020-01-07 00:29:48 +01:00
Jörg Breitbart fc2df6cfd6 all multiple @vt in one multiline comment 2020-01-06 23:58:30 +01:00
Jörg Breitbart 5b2ccdbda7 make linter happy 2020-01-05 23:00:46 +01:00
Jörg Breitbart a0d724c186 move doc to impl 2020-01-05 22:41:36 +01:00
Jörg Breitbart aec2baa7e7 Merge branch 'master' into extract_vt_features 2020-01-05 21:38:21 +01:00
Jörg Breitbart 67bbbd8671 Merge branch 'master' into window_manipulation 2020-01-05 16:40:38 +01:00
Jörg Breitbart 3e1dbd4c39 fix api test 2020-01-04 15:39:00 +01:00
Jörg Breitbart df4b07f9f2 type fixes & docs 2020-01-04 14:47:14 +01:00
Daniel Imms bd4c2da37c Merge branch 'master' into updated_wcwidth 2020-01-03 07:23:44 -08:00
Daniel Imms 1c06e576ae Merge pull request #2663 from thefallentree/patch-1
Adding FluffOS to xtermjs user list
2020-01-03 07:17:16 -08:00
Yucong Sun 5d3629173f Merge branch 'master' into patch-1 2020-01-02 11:58:34 -08:00
Yucong Sun 5f25f40db9 Adding FluffOS to xtermjs user list 2020-01-02 11:57:45 -08:00
javacs3 a4efefbf5e refactor: serialize-addon, move SerializeAddon.benchmark.ts into addons/xterm-addon-serialize and add README.md 2019-12-31 22:06:19 +08:00
jerch 56e77d3106 Merge branch 'master' into updated_wcwidth 2019-12-29 11:21:55 +01:00
Daniel Imms 2a9e16bb8e Merge pull request #2654 from Tyriar/2646_attach_es6
Target es5 in attach addon
2019-12-27 08:37:46 -08:00
Daniel Imms c512485514 Target es5 in attach addon
Fixes #2646
2019-12-27 08:33:42 -08:00
jerch a0fb880ebc Merge branch 'master' into updated_wcwidth 2019-12-26 20:11:18 +01:00
jerch e52eafa7bb Merge pull request #2644 from jerch/fullwidth_erase
Correctly reset cells of previous fullwidth chars during buffer writes
2019-12-26 20:08:50 +01:00
Jörg Breitbart 9f8ea28db4 sanity checks in print wide char handling 2019-12-26 20:04:11 +01:00
Jörg Breitbart 1c1eaa6296 Merge branch 'master' into fullwidth_erase 2019-12-26 19:49:17 +01:00
Daniel Imms b32bfedde9 Merge pull request #2653 from Tyriar/2604_color_naming
Standardize how colors helper lib is structured
2019-12-27 03:22:54 +11:00
Daniel Imms f7d36a670c Merge pull request #2651 from Tyriar/2610_register_add
Use register over add for APIs returning disposables
2019-12-27 03:10:41 +11:00
Daniel Imms 37461c2a5a Merge branch 'master' into 2604_color_naming 2019-12-26 08:10:25 -08:00
Daniel Imms 5a17825b47 Merge pull request #2629 from ivanwonder/convert-color
format color value to style '#rrggbbaa'
2019-12-27 03:07:00 +11:00
Daniel Imms 761c25fa6d Merge pull request #2650 from Tyriar/2599_inverse_selection
Set glyph fg color based on original bg, not selection
2019-12-27 03:06:05 +11:00
Daniel Imms c40df65d13 Merge branch 'master' into convert-color 2019-12-27 03:02:58 +11:00
Daniel Imms 79860e7b15 Remove .only 2019-12-27 02:59:27 +11:00
Daniel Imms ca68a38f2c Merge branch 'master' into 2604_color_naming 2019-12-27 02:58:22 +11:00
Daniel Imms b075cc5e6f Merge branch 'master' into 2599_inverse_selection 2019-12-27 02:55:51 +11:00
Daniel Imms 3e3c51ae53 Add a test for selection
Part of #2600
2019-12-26 14:26:53 +11:00
Daniel Imms 28d7eddcb4 Set glyph fg color based on original bg, not selection
This involves resolving the rgb channels of the original background
color and encoding them using the RGB color mode.

Fixes #2599
2019-12-26 14:04:50 +11:00
Daniel Imms 509327b4c7 Use register over add for APIs returning disposables
Fixes #2610
2019-12-26 12:19:03 +11:00
Daniel Imms ee74130806 Standardize how colors helper lib is structured
Fixes #2604
2019-12-26 12:12:08 +11:00
Jörg Breitbart 0edbbf9fe0 fix print handler; tests 2019-12-21 23:07:12 +01:00
Jörg Breitbart 7ec658a3f8 apply erase attrs in handler methods 2019-12-21 21:57:39 +01:00
Jörg Breitbart 2b4da1f88e add wide char handling to bufferline primitives 2019-12-21 21:40:01 +01:00
Jörg Breitbart 84608256ea Merge branch 'master' into window_manipulation 2019-12-21 17:11:12 +01:00
Jörg Breitbart 0f8a034e8b make linter happy 2019-12-21 16:29:46 +01:00
Jörg Breitbart 0631323bf5 cleanup 2019-12-21 16:27:12 +01:00
Jörg Breitbart 825cc006e5 Merge branch 'master' into updated_wcwidth 2019-12-21 15:28:38 +01:00
Daniel Imms f1e01c7166 Merge pull request #2640 from Tyriar/reset_fix
Move back to reseting parser only on RIS
2019-12-20 06:26:50 +11:00
Daniel Imms 15e3173177 Move back to reseting parser only on RIS
Related #2637
2019-12-20 06:14:26 +11:00
javacs3 34bf2f9a0f refactor: serialize-addon, remove timeout in api test 2019-12-19 22:17:32 +08:00
Daniel Imms 71a78cbf01 Merge pull request #2637 from Tyriar/input_handler
Remove a large portion of InputHandler's dependency on Terminal
2019-12-19 18:45:13 +11:00
Daniel Imms 03fc6c3f97 Don't keep a reference to parent anymore 2019-12-19 16:04:15 +11:00
Daniel Imms e6c095754f Remove old params props from Terminal 2019-12-19 15:59:59 +11:00
Daniel Imms d045d39f88 Move charset service reset out of _setup 2019-12-19 15:58:27 +11:00
Daniel Imms 63bf719067 Merge branch 'master' into input_handler 2019-12-19 15:54:03 +11:00
Daniel Imms 34168bd223 Fix InputHandler resetting 2019-12-19 15:42:23 +11:00
Daniel Imms 4f3e3d9285 Merge pull request #2631 from cmeyer90/master
Added Gus to list of xterm real-world users
2019-12-19 15:20:47 +11:00
Daniel Imms c41f1c3d22 Merge branch 'master' into master 2019-12-19 15:20:23 +11:00
Daniel Imms 7d8086e138 Merge pull request #2636 from phillc/master
Add Linode to real world uses
2019-12-19 15:19:24 +11:00
Phillip Campbell 0727797254 Add Linode to real world uses 2019-12-18 09:05:29 -05:00
javacs3 7a8c3a42cd refactor: serialize-addon, rename some APIs in SerializeAddon 2019-12-18 20:27:44 +08:00
javacs3 84f5f20edb refactor: serialize-addon, remove equalFg(), equalBg(), equalFlags() api from xterm.d.ts 2019-12-18 20:27:39 +08:00
javacs3 b80b5a94f9 refactor: serialize-addon, rename _nextRow() to _rowEnd() 2019-12-18 20:27:33 +08:00
javacs3 a43cc23d67 refactor: serialize-addon, remove _cellFgBgChanged(), _cellFlagsChanged(), _lineStart() functions 2019-12-18 20:27:19 +08:00
javacs3 9e1f607bd9 fix: serialize-addon, remove unused packages in yarn.lock 2019-12-18 20:27:01 +08:00
Daniel Imms 914861503b Remove a bunch of unused interfaces 2019-12-18 12:06:41 +11:00
Daniel Imms 8e996ae571 Remove terminal.bell usage 2019-12-18 12:00:38 +11:00
Daniel Imms 5d351ec8c0 Isolate cast in TestInputHandler 2019-12-18 11:57:16 +11:00
Daniel Imms 33db0203dd Remove terminal.reset usage from input handler 2019-12-18 11:55:25 +11:00
Daniel Imms 7a1bef7daf More cur/erase attr data to input handler 2019-12-18 11:47:45 +11:00
Daniel Imms 2bbda38488 Remove redundant value reset
Done in core service reset
2019-12-18 11:31:08 +11:00
Daniel Imms e3e59c35fc Use optional chaining in InputHandler 2019-12-18 11:28:15 +11:00
Daniel Imms dc61fae12a Move applicationKeypad into core service 2019-12-18 11:23:57 +11:00
Daniel Imms dada7fe728 Move origin mode into core service 2019-12-18 11:20:36 +11:00
Daniel Imms bff74b1d7d Move wraparound mode into core service modes 2019-12-18 11:16:27 +11:00
Daniel Imms 224fb5c6a2 Move charset into own service 2019-12-18 10:53:05 +11:00
Daniel Imms 05e1cd8478 Move charset logic into core service 2019-12-18 10:32:38 +11:00
Daniel Imms a2297a5fa6 Remove Terminal.refresh usage in InputHandler 2019-12-18 10:30:49 +11:00
Daniel Imms 94f5c37884 Support events with 2 args
This will allow higher perf events by avoiding object creation
2019-12-18 09:59:33 +11:00
Clark Meyer ed334fd790 Added Gus to list of xterm real-world users 2019-12-14 21:59:43 -08:00
ivanwonder efb2069500 format color value to style '#rrggbbaa' 2019-12-13 10:57:42 +08:00
Daniel Imms 924526c1e6 Merge pull request #2628 from Tyriar/webgl_pub
Webgl v0.4.1
2019-12-12 09:48:55 +11:00
Daniel Imms 89995fe7ca Webgl v0.4.1 2019-12-11 14:47:26 -08:00
Daniel Imms 929914da5d Merge pull request #2626 from Tyriar/2634_texture_atlas
Expose texture atlas as API and use in demo
2019-12-12 09:45:19 +11:00
Daniel Imms 31669cbad6 Expose texture atlas as API and use in demo
Part of #2623
2019-12-11 14:26:12 -08:00
Daniel Imms a1e73331b8 Merge pull request #2625 from Tyriar/2596_hidden
Implement hidden in DOM and WebGL renderers
2019-12-10 23:07:30 -08:00
Daniel Imms 31da88ad89 Add webgl invisible tests 2019-12-09 19:47:47 -08:00
Daniel Imms 93cdee3bab Support hidden in WebGL renderer
Fixes #2596
2019-12-09 19:39:59 -08:00
Daniel Imms ed7d8e7c3b Support hidden attr in DOM renderer 2019-12-09 19:37:12 -08:00
Daniel Imms 050d9ff04d Merge pull request #2621 from jerch/update_nodepty
update version of node-pty
2019-12-08 10:46:25 -08:00
jerch 512076898d Merge branch 'master' into window_manipulation 2019-12-08 18:58:57 +01:00
Jörg Breitbart a628e427de update version of node-pty 2019-12-08 18:46:40 +01:00
Daniel Imms 05f1887559 Merge branch 'master' into updated_wcwidth 2019-12-07 13:32:13 -08:00
Daniel Imms 266859edf3 Merge pull request #2590 from npezza93/configure-cursor-bar-width
Allow the thickness of the bar cursor to be configured
2019-12-07 13:30:17 -08:00
Daniel Imms 79ea49cafd Floor cursorWidth to ensure it's an integer 2019-12-07 13:25:59 -08:00
Nick Pezza 3cb67f2806 Merge branch 'master' into configure-cursor-bar-width 2019-12-07 15:54:35 -05:00
Nick Pezza fa4de08934 Apply changes to the dom renderer 2019-12-07 15:54:17 -05:00
Nick Pezza 70c8f8733a Sanitize cursorWidth option 2019-12-07 15:42:33 -05:00
Nick Pezza 97dc61e65f s/cursorBarWidth/cursorWidth 2019-12-07 15:40:02 -05:00
Nick Pezza 48aebec01f Remove default width value 2019-12-07 15:39:34 -05:00
Nick PezzaandDaniel Imms 58078459f8 Update cursor width doc
Co-Authored-By: Daniel Imms <tyriar@tyriar.com>
2019-12-07 15:38:12 -05:00
Daniel Imms fb9b19b95e Merge remote-tracking branch 'ups/feat/serialize-addon' into pr/JavaCS3/2369 2019-12-07 10:27:11 -08:00
Daniel Imms 7af6cf450b Merge remote-tracking branch 'origin/master' into feat/serialize-addon 2019-12-07 10:26:38 -08:00
Daniel Imms ca12cf5802 Support serializing tabs 2019-12-07 10:24:22 -08:00
Jörg Breitbart d7737f6c88 cleanup 2019-12-07 13:39:06 +01:00
Jörg Breitbart b5fdd3b8d8 several fixes 2019-12-07 13:23:00 +01:00
Jörg Breitbart 5b95f7e565 Merge branch 'window_manipulation' of github.com:jerch/xterm.js into window_manipulation 2019-12-07 12:26:51 +01:00
Jörg Breitbart be3f3da532 Merge branch 'master' into window_manipulation 2019-12-07 12:26:31 +01:00
Jörg Breitbart f13e52fffb Merge branch 'master' into updated_wcwidth 2019-12-07 12:21:18 +01:00
Daniel Imms ab66cb5d26 Merge pull request #2616 from sdegutis/patch-1
Avoid roundtrip to browser when double-disposing.
2019-12-06 16:18:30 -08:00
Jörg Breitbart 878b762537 Merge branch 'master' into updated_wcwidth 2019-12-06 23:17:09 +01:00
Steven Degutis c5e144f327 Avoid roundtrip to browser when double-disposing. 2019-12-06 15:57:26 -06:00
Jörg Breitbart 92ac99e878 cleanup 2019-12-06 20:41:24 +01:00
Jörg Breitbart 70dabf11db fix failing tests 2019-12-06 17:00:46 +01:00
Jörg Breitbart 0f83d645b6 remove V11 from base repo 2019-12-06 16:46:50 +01:00
Jörg Breitbart 794573969c remove CharWidth.ts 2019-12-06 16:44:20 +01:00
Jörg Breitbart 303abf8a91 unicode11 addon 2019-12-06 16:28:16 +01:00
Jörg Breitbart 43303ba4c1 add interfaces and option handling 2019-12-05 21:30:59 +01:00
Jörg Breitbart 31ccb796ac Merge branch 'master' into updated_wcwidth 2019-12-05 15:45:52 +01:00
Jörg Breitbart 3395ccb943 UnicodeService & provider 2019-12-05 15:42:55 +01:00
Nick Pezza c630b92772 Allow the thickness of the bar cursor to be configured 2019-11-30 14:40:45 -05:00
javacs3 903d75e6d3 Refactor
1. add 8 secs timeout for SerializeAddon API test
2. move demo page's addons control panel into a single div
3. remove unnecessary config in demo page
4. remove v8-profiler-node8 dependency
5. remove duplicate css option
2019-11-28 22:22:04 +08:00
Daniel Imms 0be6d9f048 Merge branch 'master' into window_manipulation 2019-11-26 08:08:28 -08:00
Jörg Breitbart a574591481 Merge branch 'updated_wcwidth' of github.com:jerch/xterm.js into updated_wcwidth 2019-11-26 02:04:36 +01:00
Jörg Breitbart 435d480cf1 unicode service stub 2019-11-26 01:59:10 +01:00
Daniel Imms 3969eb9382 Add failing tab test 2019-11-24 10:20:19 -08:00
Daniel Imms 45af02590e Make serialize addon api tests fast 2019-11-24 10:15:38 -08:00
Daniel Imms 709214a414 Add webpacked version to client 2019-11-24 10:06:41 -08:00
Daniel Imms 6dda0c2b97 Merge branch 'feat/serialize-addon' into pr/JavaCS3/2369 2019-11-24 10:05:14 -08:00
Daniel Imms 20578e0a43 Merge remote-tracking branch 'origin/master' into feat/serialize-addon 2019-11-24 09:55:31 -08:00
jerch 5a1e8484a9 Merge branch 'master' into window_manipulation 2019-11-23 17:37:38 +01:00
javacs3 1093b7c3d3 fix lint error 2019-11-21 20:53:53 +08:00
javacs3 7ec99c2469 update IBufferCell API 2019-11-21 20:41:48 +08:00
javacs3 3543155953 fix typo 2019-11-21 20:41:48 +08:00
javacs3 e2df7dc96c add benchmark for serialize addon 2019-11-21 20:41:48 +08:00
javacs3 cd5f3e9740 update cell color api design 2019-11-21 20:41:47 +08:00
javacs3 ae70ef0fa2 resolve SerializeAddon importing workaround 2019-11-21 20:41:47 +08:00
javacs3 f1a17e3cf5 refactor CellColor constructor 2019-11-21 20:41:47 +08:00
javacs3 22a2496360 fix 'Can't resolve 'xterm' in xterm.js/addons/xterm-addon-serialize/out' 2019-11-21 20:41:47 +08:00
javacs3 7a7f3466f3 refactor ICellColor to CellColor 2019-11-21 20:41:47 +08:00
javacs3 73f9d45fdc refactor use a nicer IBufferCell design 2019-11-21 20:41:47 +08:00
javacs3 3cba961dad refactor SerializeAddon: use public api as much as possible 2019-11-21 20:41:47 +08:00
javacs3 6710c13cc2 fix cell.getFgColor/getBgColor return color256 code even it's color16
mode leading to serialize return wrong sequence
2019-11-21 20:41:47 +08:00
javacs3 f12120b055 fix demo page <code> tag won't display multi space chars 2019-11-21 20:41:46 +08:00
javacs3 693f30477e add trim last empty lines support for SerializeAddon 2019-11-21 20:41:46 +08:00
javacs3 92709110ef add SerializeAddon in demo page 2019-11-21 20:41:46 +08:00
javacs3 d81b4999dc add frontground/background color support for SerializeAddon 2019-11-21 20:41:46 +08:00
javacs3 f088a9953f refactor SerializeAddon extract SerializeHandler 2019-11-21 20:41:46 +08:00
javacs3 a069df7a36 refactor SerializeAddons initial value validation 2019-11-21 20:41:46 +08:00
jerch 7f26b04ea1 Merge branch 'master' into window_manipulation 2019-11-16 00:05:42 +01:00
jerch f81cca67eb Merge branch 'master' into updated_wcwidth 2019-11-16 00:03:45 +01:00
Jörg Breitbart aca6d24e96 change to v11, expose unicodeVersion, fix linkifier 2019-11-15 18:08:17 +01:00
Jörg Breitbart 25bbde2be2 cleanup CharWidth.ts 2019-11-15 14:47:51 +01:00
Jörg Breitbart c7ad4aa460 terminal option unicodeVersion 2019-11-15 13:33:28 +01:00
Jörg Breitbart 9c9aa68c23 wcwidth rules unicode 10 2019-11-15 01:07:14 +01:00
jerch 6cc6cb5ffd Merge branch 'master' into window_manipulation 2019-11-14 22:43:51 +01:00
Jörg Breitbart 7ad2a68052 cleanup 2019-11-14 13:05:24 +01:00
Jörg Breitbart 8991ed8270 simplify options 2019-11-14 12:59:48 +01:00
Jörg Breitbart 45d1139ab0 windowOptions on ctor options 2019-11-14 12:20:59 +01:00
Jörg Breitbart 26052a2963 fix api test 2019-11-14 11:38:18 +01:00
Jörg Breitbart eeb9270882 change WindowOptions type 2019-11-14 11:23:21 +01:00
Jörg Breitbart 8fc2d22b28 add IWindowOptions to xterm.d.ts 2019-11-14 10:20:51 +01:00
Jörg Breitbart d09e2fbcec stub for window options 2019-11-13 23:09:52 +01:00
Jörg Breitbart 2797faae6f Merge branch 'master' into window_manipulation 2019-11-12 17:05:01 +01:00
Jörg Breitbart 98cc17df24 remove leftover .only 2019-10-29 15:57:11 +01:00
Jörg Breitbart 2cad586ccc add integration tests for pixel reports 2019-10-29 15:50:54 +01:00
Jörg Breitbart da3ae79c37 implement 14, 16, 18, 20, 21, 22, 23; restrict DECCOLM to 24 2019-10-29 13:57:36 +01:00
jerch 38f05dabe2 Merge branch 'master' into window_manipulation 2019-10-29 08:28:02 +01:00
jerch d0689abd35 Merge branch 'master' into window_manipulation 2019-10-25 00:00:44 +02:00
jerch 50b7fcb90a Merge branch 'master' into window_manipulation 2019-10-04 20:14:14 +02:00
jerch 7e01c833be Merge branch 'master' into extract_vt_features 2019-10-04 20:13:59 +02:00
jerch 308c8ba1a5 Merge branch 'master' into extract_vt_features 2019-09-12 10:55:55 +02:00
Daniel Imms 477d1b2569 Merge branch 'master' into window_manipulation 2019-09-06 10:29:00 -07:00
jerch 5c7a14cfe0 Merge branch 'master' into extract_vt_features 2019-09-05 22:41:35 +02:00
jerch fb9019e0c8 Merge branch 'master' into window_manipulation 2019-09-05 22:41:21 +02:00
Jörg Breitbart b3750bed2d early hack 2019-08-24 11:47:46 +02:00
jerch fa12649642 Merge branch 'master' into extract_vt_features 2019-08-23 20:37:29 +02:00
Jörg Breitbart aacc9c86b9 linter 2019-08-19 22:59:14 +02:00
Jörg Breitbart 8b2d85c823 document more commands; long description hook 2019-08-19 22:54:55 +02:00
Jörg Breitbart 66709e6f22 fix OSC list header; general notes 2019-08-18 21:50:17 +02:00
Jörg Breitbart f34edc3116 rename script 2019-08-18 17:57:22 +02:00
Jörg Breitbart 9cd80a2c6b cleanup 2019-08-18 17:52:26 +02:00
Jörg Breitbart 2de22f9356 extract @vt entries from docs 2019-08-18 17:36:06 +02:00
Daniel Imms 2e237cc449 Merge pull request #2298 from JavaCS3/master
add SerializeAddon
2019-08-02 14:32:03 -07:00
Daniel Imms bb734706fc Merge branch 'master' into master 2019-08-02 11:37:09 -07:00
Charles Wei 09ede0221a Merge branch 'master' into master 2019-07-16 20:03:57 +08:00
javacs3 f859bffa74 fix lint error 2019-07-15 09:42:56 +08:00
javacs3 bca90245c8 serialize from the bottom of the terminal 2019-07-14 23:28:08 +08:00
javacs3 fe3520c685 change exception message 2019-07-13 17:25:18 +08:00
javacs3 31f812cf94 add missing api docs and refactor serialize implementation by using list join instead and some code clean up 2019-07-12 23:23:06 +08:00
javacs3 57a8255654 add test files from addons folder 2019-07-12 23:23:06 +08:00
Daniel Imms a71ed7b5b7 Merge branch 'master' into master 2019-07-10 17:14:13 -07:00
javacs3 3ba6ffe5f4 fix lint error 2019-07-10 21:57:50 +08:00
javacs3 2571a0654f add SerializeAddon 2019-07-10 21:46:51 +08:00
92 changed files with 4765 additions and 1295 deletions
+3
View File
@@ -156,6 +156,9 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
- [**Gus**](https://gus.jp): A shared coding pad where you can run Python with xterm.js
- [**Linode**](https://linode.com): Linode uses xterm.js to provide users a web console for their Linode instances.
- [**FluffOS**](https://www.fluffos.info): Active maintained LPMUD driver with websocket support.
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-attach",
"version": "0.4.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"target": "es5",
"lib": [
"dom",
"es2015"
+2 -2
View File
@@ -320,13 +320,13 @@ export class SearchAddon implements ITerminalAddon {
break;
}
// Adjust the searchIndex to normalize emoji into single chars
const char = cell.char;
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.width;
const charWidth = cell.getWidth();
if (charWidth === 0) {
resultIndex++;
}
+3
View File
@@ -0,0 +1,3 @@
lib
node_modules
out-benchmark
+5
View File
@@ -0,0 +1,5 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
+42
View File
@@ -0,0 +1,42 @@
## xterm-addon-serialize
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables xterm.js to serialize a terminal framebuffer into string or html. This addon requires xterm.js v4+.
⚠️ This is an experimental addon that is still under construction ⚠️
### Install
```bash
npm install --save xterm-addon-serialize
```
### Usage
```ts
import { Terminal } from "xterm";
import { SerializeAddon } from "xterm-addon-serialize";
const terminal = new Terminal();
const serializeAddon = new SerializeAddon();
terminal.loadAddon(serializeAddon);
terminal.write("something...", () => {
console.log(serializeAddon.serialize());
});
```
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts) for more advanced usage.
### Benchmark
⚠️ Ensure you have `lolcat`, `hexdump` programs installed in your computer
```shell
$ git clone https://github.com/xtermjs/xterm.js.git
$ cd xterm.js
$ yarn
$ cd addons/xterm-addon-serialize
$ yarn benchmark && yarn benchmark-baseline
$ # change some code in `xterm-addon-serialize`
$ yarn benchmark-eval
```
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
import { spawn } from 'node-pty';
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
import { Terminal } from 'public/Terminal';
import { SerializeAddon } from 'SerializeAddon';
class TestTerminal extends Terminal {
writeSync(data: string): void {
(<any>this)._core.writeSync(data);
}
}
perfContext('Terminal: sh -c "dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f"', () => {
let content = '';
let contentUtf8: Uint8Array;
before(async () => {
const p = spawn('sh', ['-c', 'dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f'], {
name: 'xterm-256color',
cols: 80,
rows: 25,
cwd: process.env.HOME,
env: process.env,
encoding: (null as unknown as string) // needs to be fixed in node-pty
});
const chunks: Buffer[] = [];
let length = 0;
p.on('data', data => {
chunks.push(data as unknown as Buffer);
length += data.length;
});
await new Promise(resolve => p.on('exit', () => resolve()));
contentUtf8 = Buffer.concat(chunks, length);
// translate to content string
const buffer = new Uint32Array(contentUtf8.length);
const decoder = new Utf8ToUtf32();
const codepoints = decoder.decode(contentUtf8, buffer);
for (let i = 0; i < codepoints; ++i) {
content += stringFromCodePoint(buffer[i]);
// peek into content to force flat repr in v8
if (!(i % 10000000)) {
content[i];
}
}
});
perfContext('serialize', () => {
let terminal: TestTerminal;
const serializeAddon = new SerializeAddon();
before(() => {
terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 });
serializeAddon.activate(terminal);
terminal.writeSync(content);
});
new ThroughputRuntimeCase('', () => {
return { payloadSize: serializeAddon.serialize().length };
}, { fork: false }).showAverageThroughput();
});
});
@@ -0,0 +1,19 @@
{
"APP_PATH": ".benchmark",
"evalConfig": {
"tolerance": {
"*": [0.75, 1.5],
"*.dev": [0.01, 1.5],
"*.cv": [0.01, 1.5],
"EscapeSequenceParser.benchmark.js.*.averageThroughput.mean": [0.9, 5]
},
"skip": [
"*.median",
"*.runs",
"*.dev",
"*.cv",
"EscapeSequenceParser.benchmark.js.*.averageRuntime",
"Terminal.benchmark.js.*.averageRuntime"
]
}
}
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"lib": ["dom", "es6"],
"outDir": "../out-benchmark",
"types": ["../../../node_modules/@types/node"],
"moduleResolution": "node",
"strict": false,
"target": "es2015",
"module": "commonjs",
"baseUrl": ".",
"paths": {
"common/*": ["../../../src/common/*"],
"browser/*": ["../../../src/browser/*"],
"public/*": ["../../../src/public/*"],
"Terminal": ["../../../src/Terminal"],
"SerializeAddon": ["../src/SerializeAddon"]
}
},
"include": ["../**/*", "../../../typings/xterm.d.ts", "../../../out/**/*"],
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
"references": [
{ "path": "../../../src/common" },
{ "path": "../../../src/browser" }
]
}
+23
View File
@@ -0,0 +1,23 @@
{
"name": "xterm-addon-serialize",
"version": "0.1.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-serialize.js",
"types": "typings/xterm-addon-serialize.d.ts",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package",
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js"
},
"peerDependencies": {
"xterm": "^3.14.0"
}
}
@@ -0,0 +1,370 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const width = 800;
const height = 600;
describe('SerializeAddon', () => {
before(async function (): Promise<any> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
args: [`--window-size=${width},${height}`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal({ rows: 10, cols: 10, rendererType: 'dom' });
await page.evaluate(`
window.serializeAddon = new SerializeAddon();
window.term.loadAddon(window.serializeAddon);
`);
});
after(async () => await browser.close());
beforeEach(async () => await page.evaluate(`window.term.reset()`));
it('empty content', async function (): Promise<any> {
const rows = 10;
const cols = 10;
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
});
it('trim last empty lines', async function (): Promise<any> {
const cols = 10;
const lines = [
'',
'',
digitsString(cols),
digitsString(cols),
'',
'',
digitsString(cols),
digitsString(cols),
'',
'',
''
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n'));
});
it('digits content', async function (): Promise<any> {
const rows = 10;
const cols = 10;
const digitsLine = digitsString(cols);
const lines = newArray<string>(digitsLine, rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize half rows of content', async function (): Promise<any> {
const rows = 10;
const halfRows = rows >> 1;
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n'));
});
it('serialize 0 rows of content', async function (): Promise<any> {
const rows = 10;
const cols = 10;
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), '');
});
it('serialize all rows of content with color16', async function (): Promise<any> {
const cols = 10;
const color16 = [
30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color
90, 91, 92, 93, 94, 95, 96, 97,
40, 41, 42, 43, 44, 45, 46, 47, // Set background color
100, 101, 103, 104, 105, 106, 107
];
const rows = color16.length;
const lines = newArray<string>(
(index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`),
rows
);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with fg/bg flags', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
mkSGR(INVERSE) + line,
mkSGR(BOLD) + line,
mkSGR(UNDERLINED) + line,
mkSGR(BLINK) + line,
mkSGR(INVISIBLE) + line,
mkSGR(NO_INVERSE) + line,
mkSGR(NO_BOLD) + line,
mkSGR(NO_UNDERLINED) + line,
mkSGR(NO_BLINK) + line,
mkSGR(NO_INVISIBLE) + line
];
const rows = lines.length;
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256', async function (): Promise<any> {
const rows = 32;
const cols = 10;
const lines = newArray<string>(
(index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`),
rows
);
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style separately', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_P16_RED) + line, // fg Red,
mkSGR(UNDERLINED) + line, // fg Red, Underlined
mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
mkSGR(NO_INVERSE) + line, // fg Green, Underlined
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse
mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse
mkSGR(BG_RESET) + line, // Underlined, Inverse
mkSGR(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color16 and style together', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_P16_RED) + line, // fg Red
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
mkSGR(BG_RESET) + line, // Italic
mkSGR(NORMAL) + line, // Back to normal
mkSGR(FG_P16_RED) + line, // fg Red
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
mkSGR(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256 and style separately', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_P256_RED) + line, // fg Red 256,
mkSGR(UNDERLINED) + line, // fg Red 256, Underlined
mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse
mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse
mkSGR(BG_RESET) + line, // Underlined, Inverse
mkSGR(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with color256 and style together', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_P256_RED) + line, // fg Red 256
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
mkSGR(BG_RESET) + line, // Italic
mkSGR(NORMAL) + line, // Back to normal
mkSGR(FG_P256_RED) + line, // fg Red 256
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
mkSGR(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with colorRGB and style separately', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_RGB_RED) + line, // fg Red RGB,
mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined
mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse
mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse
mkSGR(BG_RESET) + line, // Underlined, Inverse
mkSGR(NORMAL) + line // Back to normal
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize all rows of content with colorRGB and style together', async function (): Promise<any> {
const cols = 10;
const line = '+'.repeat(cols);
const lines: string[] = [
mkSGR(FG_RGB_RED) + line, // fg Red RGB
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
mkSGR(BG_RESET) + line, // Italic
mkSGR(NORMAL) + line, // Back to normal
mkSGR(FG_RGB_RED) + line, // fg Red RGB
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
mkSGR(BG_RESET) + line // Italic
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
});
it('serialize tabs correctly', async () => {
const lines = [
'a\tb',
'aa\tc',
'aaa\td'
];
const expected = [
'a\x1b[7Cb',
'aa\x1b[6Cc',
'aaa\x1b[5Cd'
];
await writeSync(page, lines.join('\\r\\n'));
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
const array: T[] = new Array<T>(count);
for (let i = 0; i < array.length; i++) {
if (typeof initial === 'function') {
array[i] = (<(index: number) => T>initial)(i);
} else {
array[i] = <T>initial;
}
}
return array;
}
function digitsString(length: number, from: number = 0, sgr: string = ''): string {
let s = sgr;
for (let i = 0; i < length; i++) {
s += `${(from++) % 10}`;
}
return s;
}
function mkSGR(...seq: string[]): string {
return `\x1b[${seq.join(';')}m`;
}
const NORMAL = '0';
const FG_P16_RED = '31';
const FG_P16_GREEN = '32';
const FG_P16_YELLOW = '33';
const FG_P256_RED = '38;5;196';
const FG_P256_GREEN = '38;5;46';
const FG_P256_YELLOW = '38;5;226';
const FG_RGB_RED = '38;2;255;0;0';
const FG_RGB_GREEN = '38;2;0;255;0';
const FG_RGB_YELLOW = '38;2;255;255;0';
const FG_RESET = '39';
const BG_P16_RED = '41';
const BG_P16_GREEN = '42';
const BG_P16_YELLOW = '43';
const BG_P256_RED = '48;5;196';
const BG_P256_GREEN = '48;5;46';
const BG_P256_YELLOW = '48;5;226';
const BG_RGB_RED = '48;2;255;0;0';
const BG_RGB_GREEN = '48;2;0;255;0';
const BG_RGB_YELLOW = '48;2;255;255;0';
const BG_RESET = '49';
const INVERSE = '7';
const BOLD = '1';
const UNDERLINED = '4';
const BLINK = '5';
const INVISIBLE = '8';
const NO_INVERSE = '27';
const NO_BOLD = '22';
const NO_UNDERLINED = '24';
const NO_BLINK = '25';
const NO_INVISIBLE = '28';
const ITALIC = '3';
const DIM = '2';
const NO_ITALIC = '23';
const NO_DIM = '22';
async function writeSync(page: puppeteer.Page, data: string): Promise<void> {
await page.evaluate(`
window.ready = false;
window.term.write('${data}', () => window.ready = true);
`);
await pollFor(page, 'window.ready', true);
}
async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise<void>): Promise<void> {
if (preFn) {
await preFn();
}
const result = await page.evaluate(fn);
if (result !== val) {
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, fn, val, preFn)), 10);
});
}
}
@@ -0,0 +1,187 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* (EXPERIMENTAL) This Addon is still under development
*/
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
function constrain(value: number, low: number, high: number): number {
return Math.max(low, Math.min(value, high));
}
// TODO: Refine this template class later
abstract class BaseSerializeHandler {
constructor(private _buffer: IBuffer) { }
serialize(startRow: number, endRow: number): 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;
this._beforeSerialize(endRow - startRow);
for (let row = startRow; row < endRow; row++) {
const line = this._buffer.getLine(row);
if (line) {
for (let col = 0; col < line.length; col++) {
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
if (!c) {
console.warn(`Can't get cell at row=${row}, col=${col}`);
continue;
}
this._nextCell(c, oldCell, row, col);
oldCell = c;
}
}
this._rowEnd(row);
}
this._afterSerialize();
return this._serializeString();
}
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
protected _rowEnd(row: number): void { }
protected _beforeSerialize(rows: number): void { }
protected _afterSerialize(): void { }
protected _serializeString(): string { return ''; }
}
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getFgColorMode() === cell2.getFgColorMode()
&& cell1.getFgColor() === cell2.getFgColor();
}
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.getBgColorMode() === cell2.getBgColorMode()
&& cell1.getBgColor() === cell2.getBgColor();
}
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
return cell1.isInverse() === cell2.isInverse()
&& cell1.isBold() === cell2.isBold()
&& cell1.isUnderline() === cell2.isUnderline()
&& cell1.isBlink() === cell2.isBlink()
&& cell1.isInvisible() === cell2.isInvisible()
&& cell1.isItalic() === cell2.isItalic()
&& cell1.isDim() === cell2.isDim();
}
class StringSerializeHandler extends BaseSerializeHandler {
private _rowIndex: number = 0;
private _allRows: string[] = new Array<string>();
private _currentRow: string = '';
private _nullCellCount: number = 0;
constructor(buffer: IBuffer) {
super(buffer);
}
protected _beforeSerialize(rows: number): void {
this._allRows = new Array<string>(rows);
}
protected _rowEnd(row: number): void {
this._allRows[this._rowIndex++] = this._currentRow;
this._currentRow = '';
this._nullCellCount = 0;
}
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
const sgrSeq: number[] = [];
const fgChanged = !equalFg(cell, oldCell);
const bgChanged = !equalBg(cell, oldCell);
const flagsChanged = !equalFlags(cell, oldCell);
if (fgChanged || bgChanged || flagsChanged) {
if (cell.isAttributeDefault()) {
this._currentRow += '\x1b[0m';
} else {
if (fgChanged) {
const color = cell.getFgColor();
if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
else if (cell.isFgPalette()) {
if (color >= 16) { sgrSeq.push(38, 5, color); }
else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }
}
else { sgrSeq.push(39); }
}
if (bgChanged) {
const color = cell.getBgColor();
if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
else if (cell.isBgPalette()) {
if (color >= 16) { sgrSeq.push(48, 5, color); }
else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }
}
else { sgrSeq.push(49); }
}
if (flagsChanged) {
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }
}
}
}
if (sgrSeq.length) {
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
}
// Count number of null cells encountered after the last non-null cell and move the cursor
// if a non-null cell is found (eg. \t or cursor move)
if (cell.getChars() === '') {
this._nullCellCount++;
} else if (this._nullCellCount > 0) {
this._currentRow += `\x1b[${this._nullCellCount}C`;
this._nullCellCount = 0;
}
this._currentRow += cell.getChars();
}
protected _serializeString(): string {
let rowEnd = this._allRows.length;
for (; rowEnd > 0; rowEnd--) {
if (this._allRows[rowEnd - 1]) {
break;
}
}
return this._allRows.slice(0, rowEnd).join('\r\n');
}
}
export class SerializeAddon implements ITerminalAddon {
private _terminal: Terminal | undefined;
constructor() { }
public activate(terminal: Terminal): void {
this._terminal = terminal;
}
public serialize(rows?: number): string {
// TODO: Add re-position cursor support
// TODO: Add word wrap mode support
// TODO: Add combinedData support
if (!this._terminal) {
throw new Error('Cannot use addon until it has been loaded');
}
const maxRows = this._terminal.buffer.length;
const handler = new StringSerializeHandler(this._terminal.buffer);
rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows);
return handler.serialize(maxRows - rows, maxRows);
}
public dispose(): void { }
}
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es2015"
],
"rootDir": ".",
"outDir": "../out",
"sourceMap": true,
"removeComments": true,
"baseUrl": ".",
"paths": {
"common/*": [ "../../../src/common/*" ]
},
"strict": true
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{ "path": "../../../src/common" }
]
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-serialize' {
/**
* An xterm.js addon that enables web links.
*/
export class SerializeAddon implements ITerminalAddon {
constructor();
/**
* Activates the addon
* @param terminal The terminal the addon is being loaded in.
*/
public activate(terminal: Terminal): void;
/**
* Serializes terminal rows into a string that can be written back to the terminal
* to restore the state. The cursor will also be positioned to the correct cell.
* When restoring a terminal it is best to do before `Terminal.open` is called
* to avoid wasting CPU cycles rendering incomplete frames.
* @param rows The number of rows to serialize, starting from the bottom of the
* terminal. This defaults to the number of rows in the viewport.
*/
public serialize(rows?: number): string;
/**
* Disposes the addon.
*/
public dispose(): void;
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const path = require('path');
const addonName = 'SerializeAddon';
const mainFile = 'xterm-addon-serialize.js';
module.exports = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
exclude: /node_modules/
}
]
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd'
},
mode: 'production'
};
+2
View File
@@ -0,0 +1,2 @@
lib
node_modules
+5
View File
@@ -0,0 +1,5 @@
**/*.api.js
**/*.api.ts
tsconfig.json
.yarnrc
webpack.config.js
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+23
View File
@@ -0,0 +1,23 @@
## xterm-addon-unicode11
An addon providing Unicode version 11 rules for xterm.js.
### Install
```bash
npm install --save xterm-addon-unicode11
```
### Usage
```ts
import { Terminal } from 'xterm';
import { Unicode11Addon } from 'xterm-addon-unicode11';
const terminal = new Terminal();
const unicode11Addon = new Unicode11Addon();
terminal.loadAddon(unicode11Addon);
// activate the new version
terminal.unicode.activeVersion = '11';
```
+21
View File
@@ -0,0 +1,21 @@
{
"name": "xterm-addon-unicode11",
"version": "0.1.1",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
},
"main": "lib/xterm-addon-unicode11.js",
"types": "typings/xterm-addon-unicode11.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"scripts": {
"build": "../../node_modules/.bin/tsc -p src",
"prepackage": "npm run build",
"package": "../../node_modules/.bin/webpack",
"prepublishOnly": "npm run package"
},
"peerDependencies": {
"xterm": "^4.0.0"
}
}
@@ -0,0 +1,61 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as puppeteer from 'puppeteer';
import { ITerminalOptions } from 'xterm';
import { assert } from 'chai';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const width = 800;
const height = 600;
describe('Unicode11Addon', () => {
before(async function(): Promise<any> {
this.timeout(20000);
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
});
after(async () => {
await browser.close();
});
beforeEach(async function(): Promise<any> {
this.timeout(20000);
await page.goto(APP);
await openTerminal();
});
it('wcwidth V11 emoji test', async () => {
await page.evaluate(`
window.unicode11 = new Unicode11Addon();
window.term.loadAddon(window.unicode11);
`);
// should have loaded '11'
assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', '11']);
// switch should not throw
await page.evaluate(`window.term.unicode.activeVersion = '11';`);
assert.deepEqual(await page.evaluate(`window.term.unicode.activeVersion`), '11');
// v6: 10, V11: 20
assert.deepEqual(await page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20);
});
});
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
@@ -0,0 +1,16 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* UnicodeVersionProvider for V11.
*/
import { Terminal, ITerminalAddon } from 'xterm';
import { UnicodeV11 } from './UnicodeV11';
export class Unicode11Addon implements ITerminalAddon {
public activate(terminal: Terminal): void {
terminal.unicode.register(new UnicodeV11());
}
public dispose(): void { }
}
@@ -0,0 +1,222 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IUnicodeVersionProvider } from 'xterm';
import { fill } from 'common/TypedArrayUtils';
type CharWidth = 0 | 1 | 2;
const BMP_COMBINING = [
[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD],
[0x05BF, 0x05BF], [0x05C1, 0x05C2], [0x05C4, 0x05C5],
[0x05C7, 0x05C7], [0x0600, 0x0605], [0x0610, 0x061A],
[0x061C, 0x061C], [0x064B, 0x065F], [0x0670, 0x0670],
[0x06D6, 0x06DD], [0x06DF, 0x06E4], [0x06E7, 0x06E8],
[0x06EA, 0x06ED], [0x070F, 0x070F], [0x0711, 0x0711],
[0x0730, 0x074A], [0x07A6, 0x07B0], [0x07EB, 0x07F3],
[0x07FD, 0x07FD], [0x0816, 0x0819], [0x081B, 0x0823],
[0x0825, 0x0827], [0x0829, 0x082D], [0x0859, 0x085B],
[0x08D3, 0x0902], [0x093A, 0x093A], [0x093C, 0x093C],
[0x0941, 0x0948], [0x094D, 0x094D], [0x0951, 0x0957],
[0x0962, 0x0963], [0x0981, 0x0981], [0x09BC, 0x09BC],
[0x09C1, 0x09C4], [0x09CD, 0x09CD], [0x09E2, 0x09E3],
[0x09FE, 0x09FE], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A51, 0x0A51], [0x0A70, 0x0A71], [0x0A75, 0x0A75],
[0x0A81, 0x0A82], [0x0ABC, 0x0ABC], [0x0AC1, 0x0AC5],
[0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], [0x0AE2, 0x0AE3],
[0x0AFA, 0x0AFF], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B44], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B62, 0x0B63], [0x0B82, 0x0B82],
[0x0BC0, 0x0BC0], [0x0BCD, 0x0BCD], [0x0C00, 0x0C00],
[0x0C04, 0x0C04], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0C62, 0x0C63],
[0x0C81, 0x0C81], [0x0CBC, 0x0CBC], [0x0CBF, 0x0CBF],
[0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], [0x0CE2, 0x0CE3],
[0x0D00, 0x0D01], [0x0D3B, 0x0D3C], [0x0D41, 0x0D44],
[0x0D4D, 0x0D4D], [0x0D62, 0x0D63], [0x0DCA, 0x0DCA],
[0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], [0x0E31, 0x0E31],
[0x0E34, 0x0E3A], [0x0E47, 0x0E4E], [0x0EB1, 0x0EB1],
[0x0EB4, 0x0EBC], [0x0EC8, 0x0ECD], [0x0F18, 0x0F19],
[0x0F35, 0x0F35], [0x0F37, 0x0F37], [0x0F39, 0x0F39],
[0x0F71, 0x0F7E], [0x0F80, 0x0F84], [0x0F86, 0x0F87],
[0x0F8D, 0x0F97], [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6],
[0x102D, 0x1030], [0x1032, 0x1037], [0x1039, 0x103A],
[0x103D, 0x103E], [0x1058, 0x1059], [0x105E, 0x1060],
[0x1071, 0x1074], [0x1082, 0x1082], [0x1085, 0x1086],
[0x108D, 0x108D], [0x109D, 0x109D], [0x1160, 0x11FF],
[0x135D, 0x135F], [0x1712, 0x1714], [0x1732, 0x1734],
[0x1752, 0x1753], [0x1772, 0x1773], [0x17B4, 0x17B5],
[0x17B7, 0x17BD], [0x17C6, 0x17C6], [0x17C9, 0x17D3],
[0x17DD, 0x17DD], [0x180B, 0x180E], [0x1885, 0x1886],
[0x18A9, 0x18A9], [0x1920, 0x1922], [0x1927, 0x1928],
[0x1932, 0x1932], [0x1939, 0x193B], [0x1A17, 0x1A18],
[0x1A1B, 0x1A1B], [0x1A56, 0x1A56], [0x1A58, 0x1A5E],
[0x1A60, 0x1A60], [0x1A62, 0x1A62], [0x1A65, 0x1A6C],
[0x1A73, 0x1A7C], [0x1A7F, 0x1A7F], [0x1AB0, 0x1ABE],
[0x1B00, 0x1B03], [0x1B34, 0x1B34], [0x1B36, 0x1B3A],
[0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B6B, 0x1B73],
[0x1B80, 0x1B81], [0x1BA2, 0x1BA5], [0x1BA8, 0x1BA9],
[0x1BAB, 0x1BAD], [0x1BE6, 0x1BE6], [0x1BE8, 0x1BE9],
[0x1BED, 0x1BED], [0x1BEF, 0x1BF1], [0x1C2C, 0x1C33],
[0x1C36, 0x1C37], [0x1CD0, 0x1CD2], [0x1CD4, 0x1CE0],
[0x1CE2, 0x1CE8], [0x1CED, 0x1CED], [0x1CF4, 0x1CF4],
[0x1CF8, 0x1CF9], [0x1DC0, 0x1DF9], [0x1DFB, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2064],
[0x2066, 0x206F], [0x20D0, 0x20F0], [0x2CEF, 0x2CF1],
[0x2D7F, 0x2D7F], [0x2DE0, 0x2DFF], [0x302A, 0x302D],
[0x3099, 0x309A], [0xA66F, 0xA672], [0xA674, 0xA67D],
[0xA69E, 0xA69F], [0xA6F0, 0xA6F1], [0xA802, 0xA802],
[0xA806, 0xA806], [0xA80B, 0xA80B], [0xA825, 0xA826],
[0xA8C4, 0xA8C5], [0xA8E0, 0xA8F1], [0xA8FF, 0xA8FF],
[0xA926, 0xA92D], [0xA947, 0xA951], [0xA980, 0xA982],
[0xA9B3, 0xA9B3], [0xA9B6, 0xA9B9], [0xA9BC, 0xA9BD],
[0xA9E5, 0xA9E5], [0xAA29, 0xAA2E], [0xAA31, 0xAA32],
[0xAA35, 0xAA36], [0xAA43, 0xAA43], [0xAA4C, 0xAA4C],
[0xAA7C, 0xAA7C], [0xAAB0, 0xAAB0], [0xAAB2, 0xAAB4],
[0xAAB7, 0xAAB8], [0xAABE, 0xAABF], [0xAAC1, 0xAAC1],
[0xAAEC, 0xAAED], [0xAAF6, 0xAAF6], [0xABE5, 0xABE5],
[0xABE8, 0xABE8], [0xABED, 0xABED], [0xFB1E, 0xFB1E],
[0xFE00, 0xFE0F], [0xFE20, 0xFE2F], [0xFEFF, 0xFEFF],
[0xFFF9, 0xFFFB]
];
const HIGH_COMBINING = [
[0x101FD, 0x101FD], [0x102E0, 0x102E0],
[0x10376, 0x1037A], [0x10A01, 0x10A03], [0x10A05, 0x10A06],
[0x10A0C, 0x10A0F], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F],
[0x10AE5, 0x10AE6], [0x10D24, 0x10D27], [0x10F46, 0x10F50],
[0x11001, 0x11001], [0x11038, 0x11046], [0x1107F, 0x11081],
[0x110B3, 0x110B6], [0x110B9, 0x110BA], [0x110BD, 0x110BD],
[0x110CD, 0x110CD], [0x11100, 0x11102], [0x11127, 0x1112B],
[0x1112D, 0x11134], [0x11173, 0x11173], [0x11180, 0x11181],
[0x111B6, 0x111BE], [0x111C9, 0x111CC], [0x1122F, 0x11231],
[0x11234, 0x11234], [0x11236, 0x11237], [0x1123E, 0x1123E],
[0x112DF, 0x112DF], [0x112E3, 0x112EA], [0x11300, 0x11301],
[0x1133B, 0x1133C], [0x11340, 0x11340], [0x11366, 0x1136C],
[0x11370, 0x11374], [0x11438, 0x1143F], [0x11442, 0x11444],
[0x11446, 0x11446], [0x1145E, 0x1145E], [0x114B3, 0x114B8],
[0x114BA, 0x114BA], [0x114BF, 0x114C0], [0x114C2, 0x114C3],
[0x115B2, 0x115B5], [0x115BC, 0x115BD], [0x115BF, 0x115C0],
[0x115DC, 0x115DD], [0x11633, 0x1163A], [0x1163D, 0x1163D],
[0x1163F, 0x11640], [0x116AB, 0x116AB], [0x116AD, 0x116AD],
[0x116B0, 0x116B5], [0x116B7, 0x116B7], [0x1171D, 0x1171F],
[0x11722, 0x11725], [0x11727, 0x1172B], [0x1182F, 0x11837],
[0x11839, 0x1183A], [0x119D4, 0x119D7], [0x119DA, 0x119DB],
[0x119E0, 0x119E0], [0x11A01, 0x11A0A], [0x11A33, 0x11A38],
[0x11A3B, 0x11A3E], [0x11A47, 0x11A47], [0x11A51, 0x11A56],
[0x11A59, 0x11A5B], [0x11A8A, 0x11A96], [0x11A98, 0x11A99],
[0x11C30, 0x11C36], [0x11C38, 0x11C3D], [0x11C3F, 0x11C3F],
[0x11C92, 0x11CA7], [0x11CAA, 0x11CB0], [0x11CB2, 0x11CB3],
[0x11CB5, 0x11CB6], [0x11D31, 0x11D36], [0x11D3A, 0x11D3A],
[0x11D3C, 0x11D3D], [0x11D3F, 0x11D45], [0x11D47, 0x11D47],
[0x11D90, 0x11D91], [0x11D95, 0x11D95], [0x11D97, 0x11D97],
[0x11EF3, 0x11EF4], [0x13430, 0x13438], [0x16AF0, 0x16AF4],
[0x16B30, 0x16B36], [0x16F4F, 0x16F4F], [0x16F8F, 0x16F92],
[0x1BC9D, 0x1BC9E], [0x1BCA0, 0x1BCA3], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0x1DA00, 0x1DA36], [0x1DA3B, 0x1DA6C],
[0x1DA75, 0x1DA75], [0x1DA84, 0x1DA84], [0x1DA9B, 0x1DA9F],
[0x1DAA1, 0x1DAAF], [0x1E000, 0x1E006], [0x1E008, 0x1E018],
[0x1E01B, 0x1E021], [0x1E023, 0x1E024], [0x1E026, 0x1E02A],
[0x1E130, 0x1E136], [0x1E2EC, 0x1E2EF], [0x1E8D0, 0x1E8D6],
[0x1E944, 0x1E94A], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
const BMP_WIDE = [
[0x1100, 0x115F], [0x231A, 0x231B], [0x2329, 0x232A],
[0x23E9, 0x23EC], [0x23F0, 0x23F0], [0x23F3, 0x23F3],
[0x25FD, 0x25FE], [0x2614, 0x2615], [0x2648, 0x2653],
[0x267F, 0x267F], [0x2693, 0x2693], [0x26A1, 0x26A1],
[0x26AA, 0x26AB], [0x26BD, 0x26BE], [0x26C4, 0x26C5],
[0x26CE, 0x26CE], [0x26D4, 0x26D4], [0x26EA, 0x26EA],
[0x26F2, 0x26F3], [0x26F5, 0x26F5], [0x26FA, 0x26FA],
[0x26FD, 0x26FD], [0x2705, 0x2705], [0x270A, 0x270B],
[0x2728, 0x2728], [0x274C, 0x274C], [0x274E, 0x274E],
[0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797],
[0x27B0, 0x27B0], [0x27BF, 0x27BF], [0x2B1B, 0x2B1C],
[0x2B50, 0x2B50], [0x2B55, 0x2B55], [0x2E80, 0x2E99],
[0x2E9B, 0x2EF3], [0x2F00, 0x2FD5], [0x2FF0, 0x2FFB],
[0x3000, 0x3029], [0x302E, 0x303E], [0x3041, 0x3096],
[0x309B, 0x30FF], [0x3105, 0x312F], [0x3131, 0x318E],
[0x3190, 0x31BA], [0x31C0, 0x31E3], [0x31F0, 0x321E],
[0x3220, 0x3247], [0x3250, 0x4DBF], [0x4E00, 0xA48C],
[0xA490, 0xA4C6], [0xA960, 0xA97C], [0xAC00, 0xD7A3],
[0xF900, 0xFAFF], [0xFE10, 0xFE19], [0xFE30, 0xFE52],
[0xFE54, 0xFE66], [0xFE68, 0xFE6B], [0xFF01, 0xFF60],
[0xFFE0, 0xFFE6]
];
const HIGH_WIDE = [
[0x16FE0, 0x16FE3], [0x17000, 0x187F7],
[0x18800, 0x18AF2], [0x1B000, 0x1B11E], [0x1B150, 0x1B152],
[0x1B164, 0x1B167], [0x1B170, 0x1B2FB], [0x1F004, 0x1F004],
[0x1F0CF, 0x1F0CF], [0x1F18E, 0x1F18E], [0x1F191, 0x1F19A],
[0x1F200, 0x1F202], [0x1F210, 0x1F23B], [0x1F240, 0x1F248],
[0x1F250, 0x1F251], [0x1F260, 0x1F265], [0x1F300, 0x1F320],
[0x1F32D, 0x1F335], [0x1F337, 0x1F37C], [0x1F37E, 0x1F393],
[0x1F3A0, 0x1F3CA], [0x1F3CF, 0x1F3D3], [0x1F3E0, 0x1F3F0],
[0x1F3F4, 0x1F3F4], [0x1F3F8, 0x1F43E], [0x1F440, 0x1F440],
[0x1F442, 0x1F4FC], [0x1F4FF, 0x1F53D], [0x1F54B, 0x1F54E],
[0x1F550, 0x1F567], [0x1F57A, 0x1F57A], [0x1F595, 0x1F596],
[0x1F5A4, 0x1F5A4], [0x1F5FB, 0x1F64F], [0x1F680, 0x1F6C5],
[0x1F6CC, 0x1F6CC], [0x1F6D0, 0x1F6D2], [0x1F6D5, 0x1F6D5],
[0x1F6EB, 0x1F6EC], [0x1F6F4, 0x1F6FA], [0x1F7E0, 0x1F7EB],
[0x1F90D, 0x1F971], [0x1F973, 0x1F976], [0x1F97A, 0x1F9A2],
[0x1F9A5, 0x1F9AA], [0x1F9AE, 0x1F9CA], [0x1F9CD, 0x1F9FF],
[0x1FA70, 0x1FA73], [0x1FA78, 0x1FA7A], [0x1FA80, 0x1FA82],
[0x1FA90, 0x1FA95], [0x20000, 0x2FFFD], [0x30000, 0x3FFFD]
];
// BMP lookup table, lazy initialized during first addon loading
let table: Uint8Array;
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
export class UnicodeV11 implements IUnicodeVersionProvider {
public readonly version = '11';
constructor() {
if (!table) {
table = new Uint8Array(65536);
fill(table, 1);
table[0] = 0;
fill(table, 0, 1, 32);
fill(table, 0, 0x7f, 0xa0);
for (let r = 0; r < BMP_COMBINING.length; ++r) {
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
}
for (let r = 0; r < BMP_WIDE.length; ++r) {
fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
}
}
}
public wcwidth(num: number): CharWidth {
if (num < 32) return 0;
if (num < 127) return 1;
if (num < 65536) return table[num] as CharWidth;
if (bisearch(num, HIGH_COMBINING)) return 0;
if (bisearch(num, HIGH_WIDE)) return 2;
return 1;
}
}
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"lib": [
"dom",
"es2015"
],
"rootDir": ".",
"outDir": "../out",
"sourceMap": true,
"removeComments": true,
"strict": true,
"baseUrl": ".",
"paths": {
"common/*": [ "../../../src/common/*" ]
},
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{ "path": "../../../src/common" }
]
}
@@ -0,0 +1,14 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ITerminalAddon } from 'xterm';
declare module 'xterm-addon-unicode11' {
export class Unicode11Addon implements ITerminalAddon {
constructor();
public activate(terminal: Terminal): void;
public dispose(): void;
}
}
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
const path = require('path');
const addonName = 'Unicode11Addon';
const mainFile = 'xterm-addon-unicode11.js';
module.exports = {
entry: `./out/${addonName}.js`,
devtool: 'source-map',
module: {
rules: [
{
test: /\.js$/,
use: ["source-map-loader"],
enforce: "pre",
exclude: /node_modules/
}
]
},
resolve: {
modules: ['./node_modules'],
extensions: [ '.js' ],
alias: {
common: path.resolve('../../out/common')
}
},
output: {
filename: mainFile,
path: path.resolve('./lib'),
library: addonName,
libraryTarget: 'umd'
},
mode: 'production'
};
@@ -26,7 +26,13 @@ const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
window.open(uri, '_blank');
const newWindow = window.open();
if (newWindow) {
newWindow.opener = null;
newWindow.location.href = uri;
} else {
console.warn('Opening link blocked as opener could not be cleared');
}
}
export class WebLinksAddon implements ITerminalAddon {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm-addon-webgl",
"version": "0.4.0",
"version": "0.5.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
+38 -6
View File
@@ -6,13 +6,14 @@
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel';
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel';
import { fill } from 'common/TypedArrayUtils';
import { slice } from './TypedArray';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants';
import { Terminal, IBufferLine } from 'xterm';
import { IColorSet } from 'browser/Types';
import { IColorSet, IColor } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types';
import { AttributeData } from 'common/buffer/AttributeData';
interface IVertices {
attributes: Float32Array;
@@ -254,18 +255,49 @@ export class GlyphRenderer {
for (let x = startCol; x < endCol; x++) {
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
const code = model.cells[offset];
let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET];
if (fg & FgFlags.INVERSE) {
const workCell = new AttributeData();
workCell.fg = fg;
workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET];
// Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors
// from bg. This is needed since the inverse fg color should be based on the original bg
// color, not on the selection color
fg = (fg & ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE));
switch (workCell.getBgColorMode()) {
case Attributes.CM_P16:
case Attributes.CM_P256:
const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba;
fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(workCell.getBgColor());
fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT;
case Attributes.CM_DEFAULT:
default:
const c2 = this._colors.background.rgba;
fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK;
}
fg |= Attributes.CM_RGB;
}
if (code & COMBINED_CHAR_BIT_MASK) {
if (!line) {
line = terminal.buffer.getLine(row);
}
const chars = line!.getCell(x)!.char;
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars);
const chars = line!.getCell(x)!.getChars();
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars);
} else {
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]);
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg);
}
}
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx >= this._colors.ansi.length) {
throw new Error('No color found for idx ' + idx);
}
return this._colors.ansi[idx];
}
public onResize(): void {
const terminal = this._terminal;
const gl = this._gl;
+8 -1
View File
@@ -10,6 +10,7 @@ import { IColorSet } from 'browser/Types';
export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
private _renderer?: WebglRenderer;
constructor(
private _preserveDrawingBuffer?: boolean
@@ -22,7 +23,8 @@ export class WebglAddon implements ITerminalAddon {
this._terminal = terminal;
const renderService: IRenderService = (<any>terminal)._core._renderService;
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer);
renderService.setRenderer(this._renderer);
}
public dispose(): void {
@@ -32,5 +34,10 @@ export class WebglAddon implements ITerminalAddon {
const renderService: IRenderService = (<any>this._terminal)._core._renderService;
renderService.setRenderer((<any>this._terminal)._core._createRenderer());
renderService.onResize(this._terminal.cols, this._terminal.rows);
this._renderer = undefined;
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._renderer?.textureAtlas;
}
}
@@ -148,6 +148,52 @@ describe('WebGL Renderer Integration Tests', function(): void {
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground 0-15 inivisible', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
green: '#070809',
yellow: '#0a0b0c',
blue: '#0d0e0f',
magenta: '#101112',
cyan: '#131415',
white: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `);
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(3, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(4, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(5, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(6, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(7, 1), [0, 0, 0, 255]);
await pollFor(page, () => getCellColor(8, 1), [0, 0, 0, 255]);
});
it('background 0-15 inivisible', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
green: '#070809',
yellow: '#0a0b0c',
blue: '#0d0e0f',
magenta: '#101112',
cyan: '#131415',
white: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground 0-15 bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
@@ -274,6 +320,46 @@ describe('WebGL Renderer Integration Tests', function(): void {
}
});
it('foreground 16-255 invisible', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]);
}
}
});
it('background 16-255 invisible', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('foreground true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
@@ -561,6 +647,42 @@ describe('WebGL Renderer Integration Tests', function(): void {
}
}
});
it('foreground true color grey invisible', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[8;38;2;${i};${i};${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]);
}
}
});
it('background true color grey invisible', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
});
describe('minimumContrastRatio', async () => {
@@ -703,6 +825,30 @@ describe('WebGL Renderer Integration Tests', function(): void {
});
});
describe('selection', async () => {
before(async () => setupBrowser());
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
it('should resolve the inverse foreground color based on the original background color, not the selection', async () => {
const theme: ITheme = {
foreground: '#FF0000',
background: '#00FF00',
selection: '#0000FF'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(` █\\x1b[7m█\\x1b[0m`);
await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]);
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]);
await page.evaluate(`window.term.selectAll()`);
// Selection only cell needs to be first to ensure renderer has kicked in
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]);
});
});
describe('allowTransparency', async () => {
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true}));
after(async () => browser.close());
@@ -100,6 +100,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
super.dispose();
}
public get textureAtlas(): HTMLCanvasElement | undefined {
return this._charAtlas?.cacheCanvas;
}
public setColors(colors: IColorSet): void {
this._colors = colors;
// Clear layers and force a full render
@@ -11,7 +11,7 @@ import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'browser/Types';
import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { toCss, ensureContrastRatioRgba } from 'browser/Color';
import { channels, rgba } from 'browser/Color';
// 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.
@@ -86,9 +86,6 @@ export class WebglCharAtlas implements IDisposable {
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
// This is useful for debugging
document.body.appendChild(this.cacheCanvas);
}
public dispose(): void {
@@ -224,7 +221,7 @@ export class WebglCharAtlas implements IDisposable {
return this._getColorFromAnsiIndex(fgColor).css;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
return toCss(arr[0], arr[1], arr[2]);
return channels.toCss(arr[0], arr[1], arr[2]);
case Attributes.CM_DEFAULT:
default:
if (inverse) {
@@ -287,14 +284,14 @@ export class WebglCharAtlas implements IDisposable {
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
if (!result) {
this._config.colors.contrastCache.setCss(bg, fg, null);
return undefined;
}
const css = toCss(
const css = channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
@@ -316,6 +313,11 @@ export class WebglCharAtlas implements IDisposable {
this._workAttributeData.fg = fg;
this._workAttributeData.bg = bg;
const invisible = !!this._workAttributeData.isInvisible();
if (invisible) {
return NULL_RASTERIZED_GLYPH;
}
const bold = !!this._workAttributeData.isBold();
const inverse = !!this._workAttributeData.isInverse();
const dim = !!this._workAttributeData.isDim();
@@ -153,11 +153,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillLeftLineAtCell(x: number, y: number): void {
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
window.devicePixelRatio,
window.devicePixelRatio * width,
this._scaledCellHeight);
}
@@ -204,7 +204,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css;
this._fillLeftLineAtCell(x, y);
this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth'));
this._ctx.restore();
}
@@ -10,6 +10,8 @@ declare module 'xterm-addon-webgl' {
* An xterm.js addon that provides search functionality.
*/
export class WebglAddon implements ITerminalAddon {
public textureAtlas?: HTMLCanvasElement;
constructor(preserveDrawingBuffer?: boolean);
/**
+2
View File
@@ -27,7 +27,9 @@ jobs:
displayName: 'Install dependencies and build'
- script: |
yarn test-unit-coverage --forbid-only
EXIT_CODE=$?
./node_modules/.bin/nyc report --reporter=cobertura
exit $EXIT_CODE
displayName: 'Unit tests'
- script: yarn lint
displayName: 'Lint'
+503
View File
@@ -0,0 +1,503 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* Script to extract vt features documented in docstrings.
*/
const fs = require('fs');
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;
/**
* 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*"(.*?)".*$/;
// known vt command types
const TYPES = [
'C0',
'C1',
'ESC',
'CSI',
'DCS',
'OSC',
'APC',
'PM',
'SOS'
];
const MARKDOWN_TMPL = `
{::options parse_block_html="true" /}
xterm.js version: {{version}}
## Table of Contents
<nav>
- [General notes](#general-notes)
{{#C0.length}}
- [C0](#c0)
{{/C0.length}}
{{#C1.length}}
- [C1](#c1)
{{/C1.length}}
{{#CSI.length}}
- [CSI](#csi)
{{/CSI.length}}
{{#DCS.length}}
- [DCS](#dcs)
{{/DCS.length}}
{{#ESC.length}}
- [ESC](#esc)
{{/ESC.length}}
{{#OSC.length}}
- [OSC](#osc)
{{/OSC.length}}
</nav>
## General notes
This document lists xterm.js' support of terminal sequences. The sequences are grouped by their sequence type:
- C0: single byte command (7bit control codes, byte range \`\\x00\` .. \`\\x1F\`, \`\\x7F\`)
- C1: single byte command (8bit control codes, byte range \`\\x80\` .. \`\\x9F\`)
- ESC: sequence starting with \`ESC\` (\`\\x1B\`)
- CSI - Control Sequence Introducer: sequence starting with \`ESC [\` (7bit) or CSI (\`\\x9B\`, 8bit)
- DCS - Device Control String: sequence starting with \`ESC P\` (7bit) or DCS (\`\\x90\`, 8bit)
- OSC - Operating System Command: sequence starting with \`ESC ]\` (7bit) or OSC (\`\\x9D\`, 8bit)
Application Program Command (APC), Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
any sequence of these types will be ignored. They are also not hookable by the API.
Note that the list only contains sequences implemented in xterm.js' core codebase. Missing sequences are either
not supported or unstable/experimental. Furthermore addons or integrations can provide additional custom sequences.
To denote the sequences the tables use the same abbreviations as xterm does:
- \`Ps\`: A single (usually optional) numeric parameter, composed of one or more decimal digits.
- \`Pm\`: A multiple numeric parameter composed of any number of single numeric parameters, separated by ; character(s),
e.g. \` Ps ; Ps ; ... \`.
- \`Pt\`: A text parameter composed of printable characters. Note that for most commands with \`Pt\` only
ASCII printables are specified to work. Additionally the parser will let pass any codepoint greater than C1 as printable.
{{#C0.length}}
## C0
| Mnemonic | Name | Sequence | Short Description | Support |
| -------- | ---- | -------- | ----------------- | ------- |
{{#C0}}
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/C0}}
{{#C0.hasLongDescriptions}}
{{#C0}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/C0}}
{{/C0.hasLongDescriptions}}
{{/C0.length}}
{{#C1.length}}
## C1
| Mnemonic | Name | Sequence | Short Description | Support |
| -------- | ---- | -------- | ----------------- | ------- |
{{#C1}}
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/C1}}
{{#C1.hasLongDescriptions}}
{{#C1}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/C1}}
{{/C1.hasLongDescriptions}}
{{/C1.length}}
{{#CSI.length}}
## CSI
| Mnemonic | Name | Sequence | Short Description | Support |
| -------- | ---- | -------- | ----------------- | ------- |
{{#CSI}}
| {{mnemonic}} | {{name}} | \`\`{{{sequence}}}\`\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/CSI}}
{{#CSI.hasLongDescriptions}}
{{#CSI}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/CSI}}
{{/CSI.hasLongDescriptions}}
{{/CSI.length}}
{{#DCS.length}}
## DCS
| Mnemonic | Name | Sequence | Short Description | Support |
| -------- | ---- | -------- | ----------------- | ------- |
{{#DCS}}
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/DCS}}
{{#DCS.hasLongDescriptions}}
{{#DCS}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/DCS}}
{{/DCS.hasLongDescriptions}}
{{/DCS.length}}
{{#ESC.length}}
## ESC
| Mnemonic | Name | Sequence | Short Description | Support |
| -------- | ---- | -------- | ----------------- | ------- |
{{#ESC}}
| {{mnemonic}} | {{name}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/ESC}}
{{#ESC.hasLongDescriptions}}
{{#ESC}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/ESC}}
{{/ESC.hasLongDescriptions}}
{{/ESC.length}}
{{#OSC.length}}
## OSC
**Note**: Other than listed in the table, the parser recognizes both ST (ECMA-48) and BEL (xterm) as OSC sequence finalizer.
| Identifier | Sequence | Short Description | Support |
| ---------- | -------- | ----------------- | ------- |
{{#OSC}}
| {{mnemonic}} | \`{{sequence}}\` | {{{shortDescription}}} {{#longDescription.length}}_[more](#{{longTarget}}){: .link-details}_{{/longDescription.length}} | {{{status}}} |
{{/OSC}}
{{#OSC.hasLongDescriptions}}
{{#OSC}}
{{#longDescription.length}}
<section class="sequence-details">
### {{name}}
{{#longDescription}}
{{{.}}}
{{/longDescription}}
</section>
{{/longDescription.length}}
{{/OSC}}
{{/OSC.hasLongDescriptions}}
{{/OSC.length}}
<script type="text/javascript">
const linkStates = {};
function hideDetailSections() {
for (let section of document.getElementsByClassName('sequence-details')) section.style.display = 'none';
}
function decorateDetailLinks() {
for (let link of document.getElementsByClassName('link-details')) {
link.addEventListener("click", toggleDetails, false);
if (linkStates[link.href] === undefined) {
linkStates[link.href] = {open: false, links: [], trElem: null};
}
linkStates[link.href].links.push(link);
}
}
function openDetails(link) {
const closestTr = link.closest('tr');
if (!closestTr || !closestTr.parentNode) return;
const closestSection = document.getElementById(link.href.split('#')[1]).closest('section');
if (!closestSection || !closestSection.innerHTML) return;
const content = closestSection.innerHTML.split('</h3>')[1];
const newRow = closestTr.parentNode.insertRow(closestTr.rowIndex);
const cell = newRow.insertCell(0);
cell.innerHTML = '<div style="font-style: italic">' + content + '</div>';
cell.colSpan = closestTr.children.length;
const state = linkStates[link.href];
state.trElem = newRow;
state.open = true;
if (link.innerHTML === 'more') link.innerHTML = 'less';
}
function closeDetails(link) {
const state = linkStates[link.href];
if (state.trElem && state.trElem.rowIndex !== -1) {
if (state.trElem.parentNode) state.trElem.parentNode.deleteRow(state.trElem.rowIndex-1); // why -1 here?
state.trElem.remove();
state.trElem = null;
}
state.open = false;
if (link.innerHTML === 'less') link.innerHTML = 'more';
}
function toggleDetails(ev) {
if (!ev.target || !ev.target.href) return;
const isOpen = linkStates[ev.target.href].open;
if (isOpen === undefined) return;
if (isOpen) closeDetails(ev.target);
else openDetails(ev.target);
ev.preventDefault();
return false;
}
function load() {
hideDetailSections();
decorateDetailLinks();
}
load(); // we are inlined behind all needed data, thus dont wait for onload (avoids flickering)
</script>
`
// support status marcos
// applied for:
// - status field of single @vt line
// - all lines in long description
const MACRO = [
// #Y - supported
[/#Y/g, s => '<span title="supported">✓</span>'],
// #N - unsupported
[/#N/g, s => '<span title="unsupported">✗</span>'],
// #P[reason] - partial support with a reason as title
[/#P\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Partial</span>`],
// #B[reason] - supported but broken in a certain way, reason in title
[/#B\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Broken</span>`]
];
function applyMacros(s) {
for (let i = 0; i < MACRO.length; ++i) {
s = s.replace(MACRO[i][0], MACRO[i][1]);
}
return s;
}
function replaceStatus(s) {
if (s === 'supported') return '<span title="supported">✓</span>';
if (s === 'unsupported') return '<span title="unsupported">✗</span>';
return s;
}
function createAnchorSlug(s) {
return s.toLowerCase().split(' ').join('-');
}
function empty(ar) {
return !ar.filter(Boolean, ar).length;
}
function* parseMultiLineGen(filename, s) {
if (!~s.indexOf('@vt:')) {
return;
}
const lines = s.split('\n').map(el => el.trim().replace(/[*]/, '').replace(/\s/, ''));
let grabLine = false;
let longDescription = [];
let feature = undefined;
let noLineCount = 0;
for (const line of lines) {
if (grabLine) {
if (!line) noLineCount++;
if (noLineCount >= 2) {
if (feature) {
feature.longDescription = empty(longDescription) ? [] : longDescription;
feature.longTarget = createAnchorSlug(feature.name);
yield feature;
}
grabLine = false;
longDescription = [];
feature = undefined;
noLineCount = 0;
}
else if (line.indexOf('@vt:') === 0) {
if (feature) {
feature.longDescription = empty(longDescription) ? [] : longDescription;
feature.longTarget = createAnchorSlug(feature.name);
yield feature;
}
grabLine = true;
longDescription = [];
feature = undefined;
noLineCount = 0;
} else {
//longDescription.push(line);
longDescription.push(applyMacros(line));
if (line) noLineCount = 0;
}
}
if (line.indexOf('@vt:') === 0) {
feature = parseSingleLine(filename, line);
grabLine = true;
}
}
if (grabLine && feature) {
feature.longDescription = empty(longDescription) ? [] : longDescription;
feature.longTarget = createAnchorSlug(feature.name);
yield feature;
}
}
function parseSingleLine(filename, s) {
const line = s.trim();
const match = line.match(REX_VT_LINE);
if (match !== null) {
if (!~TYPES.indexOf(match[2])) {
throw new Error(`unkown vt-command type "${match[2]}" specified in "${filename}"`);
}
return {
status: match[1],
type: match[2],
mnemonic: match[3],
name: match[4],
sequence: match[5],
shortDescription: match[6],
longDescription: [],
longTarget: '',
source: filename
};
}
}
function getSorter(entry) {
switch (entry) {
case 'C0':
case 'C1':
// NOTE: expects hex value notation at last position in sequence
return (a, b) => parseInt(a.sequence.slice(-2), 16) - parseInt(b.sequence.slice(-2), 16);
case 'OSC':
// NOTE: expects the decimal function identifier in mnemonic
return (a, b) => parseInt(a.mnemonic) - parseInt(b.mnemonic);
case 'DCS':
return (a, b) => a.mnemonic > b.mnemonic;
case 'CSI':
case 'ESC':
// default sort order by final byte
return (a, b) => {
// ugly hack to fix sorting of workaround in HPA sequence "CSI Ps ` "
// for HPA compare with length - 2 instead
const HPA = 'CSI Ps ` ';
const aa = a.sequence === HPA ? a.sequence.slice(0, -1) : a.sequence;
const bb = b.sequence === HPA ? b.sequence.slice(0, -1) : b.sequence;
return aa.charCodeAt(aa.length - 1) - bb.charCodeAt(bb.length - 1);
};
default:
return (a, b) => a.sequence > b.sequence;
}
};
function postProcessData(features) {
const featureTable = {};
for (const feature of features) {
// feature.status = replaceStatus(feature.status);
feature.status = applyMacros(feature.status);
if (featureTable[feature.type] === undefined) {
featureTable[feature.type] = [];
}
featureTable[feature.type].push(feature);
if (feature.longDescription.length) {
featureTable[feature.type].hasLongDescriptions = true;
}
}
for (const entry in featureTable) {
featureTable[entry].sort(getSorter(entry));
}
// console.error(featureTable);
featureTable.version = require('../package.json').version;
console.log(Mustache.render(MARKDOWN_TMPL, featureTable));
}
function main(filenames) {
// console.error(filenames);
let leftToProcess = filenames.length;
const features = [];
for (const filename of filenames) {
fs.readFile(filename, 'utf-8', (err, data) => {
let match;
while ((match = REX_COMMENTS.exec(data)) !== null) {
if (match.index === REX_COMMENTS.lastIndex) {
REX_COMMENTS.lastIndex++;
}
if (match[1]) {
for (let feature of parseMultiLineGen(filename, match[1])) {
if (feature) features.push(feature);
}
} else {
const feature = parseSingleLine(filename, match[2]);
if (feature) features.push(feature);
}
}
leftToProcess--;
if (!leftToProcess) {
postProcessData(features);
}
});
}
}
main(process.argv.slice(2))
+2
View File
@@ -29,6 +29,8 @@ const addonPackageDirs = [
path.resolve(__dirname, '../addons/xterm-addon-attach'),
path.resolve(__dirname, '../addons/xterm-addon-fit'),
path.resolve(__dirname, '../addons/xterm-addon-search'),
path.resolve(__dirname, '../addons/xterm-addon-serialize'),
path.resolve(__dirname, '../addons/xterm-addon-unicode11'),
path.resolve(__dirname, '../addons/xterm-addon-web-links'),
path.resolve(__dirname, '../addons/xterm-addon-webgl')
];
+2 -1
View File
@@ -14,7 +14,8 @@ env.NODE_PATH = path.resolve(__dirname, '../out');
let testFiles = [
'./out/*test.js',
'./out/**/*test.js'
'./out/**/*test.js',
'./addons/**/out/*test.js',
];
let flagArgs = [];
+45 -9
View File
@@ -12,16 +12,20 @@ import { Terminal } from '../out/public/Terminal';
import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon';
import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon';
import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon';
import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon';
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon';
// Use webpacked version (yarn package)
// import { Terminal } from '../lib/xterm';
// import { AttachAddon } from 'xterm-addon-attach';
// import { FitAddon } from 'xterm-addon-fit';
// import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
// import { SerializeAddon } from 'xterm-addon-serialize';
// import { WebLinksAddon } from 'xterm-addon-web-links';
// import { WebglAddon } from 'xterm-addon-webgl';
// import { Unicode11Addon } from 'xterm-addon-unicode11';
// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
@@ -33,8 +37,10 @@ export interface IWindowWithTerminal extends Window {
AttachAddon?: typeof AttachAddon;
FitAddon?: typeof FitAddon;
SearchAddon?: typeof SearchAddon;
SerializeAddon?: typeof SerializeAddon;
WebLinksAddon?: typeof WebLinksAddon;
WebglAddon?: typeof WebglAddon;
Unicode11Addon?: typeof Unicode11Addon;
}
declare let window: IWindowWithTerminal;
@@ -44,7 +50,7 @@ let socketURL;
let socket;
let pid;
type AddonType = 'attach' | 'fit' | 'search' | 'web-links' | 'webgl';
type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl';
interface IDemoAddon<T extends AddonType> {
name: T;
@@ -53,14 +59,18 @@ interface IDemoAddon<T extends AddonType> {
T extends 'attach' ? typeof AttachAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'serialize' ? typeof SerializeAddon :
T extends 'web-links' ? typeof WebLinksAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
typeof WebglAddon;
instance?:
T extends 'attach' ? AttachAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'serialize' ? SerializeAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
T extends 'unicode11' ? typeof Unicode11Addon :
never;
}
@@ -68,8 +78,10 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
fit: { name: 'fit', ctor: FitAddon, canChange: false },
search: { name: 'search', ctor: SearchAddon, canChange: true },
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true },
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }
};
const terminalContainer = document.getElementById('terminal-container');
@@ -113,11 +125,14 @@ if (document.location.pathname === '/test') {
window.AttachAddon = AttachAddon;
window.FitAddon = FitAddon;
window.SearchAddon = SearchAddon;
window.SerializeAddon = SerializeAddon;
window.Unicode11Addon = Unicode11Addon;
window.WebLinksAddon = WebLinksAddon;
window.WebglAddon = WebglAddon;
} else {
createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
}
function createTerminal(): void {
@@ -133,12 +148,16 @@ function createTerminal(): void {
// Load addons
const typedTerm = term as TerminalType;
addons['web-links'].instance = new WebLinksAddon();
addons.search.instance = new SearchAddon();
addons.serialize.instance = new SerializeAddon();
addons.fit.instance = new FitAddon();
typedTerm.loadAddon(addons['web-links'].instance);
typedTerm.loadAddon(addons.search.instance);
addons.unicode11.instance = new Unicode11Addon();
addons['web-links'].instance = new WebLinksAddon();
typedTerm.loadAddon(addons.fit.instance);
typedTerm.loadAddon(addons.search.instance);
typedTerm.loadAddon(addons.serialize.instance);
typedTerm.loadAddon(addons.unicode11.instance);
typedTerm.loadAddon(addons['web-links'].instance);
window.term = term; // Expose `term` to window for debugging purposes
term.onResize((size: { cols: number, rows: number }) => {
@@ -239,12 +258,10 @@ function initOptions(term: TerminalType): void {
// Internal only options
'cancelEvents',
'convertEol',
'handler',
'screenKeys',
'termName',
'useFlowControl',
// Complex option
'theme'
'theme',
'windowOptions'
];
const stringOptions = {
bellSound: null,
@@ -343,7 +360,15 @@ function initAddons(term: TerminalType): void {
if (checkbox.checked) {
addon.instance = new addon.ctor();
term.loadAddon(addon.instance);
if (name === 'webgl') {
setTimeout(() => {
document.body.appendChild((addon.instance as WebglAddon).textureAtlas);
}, 0);
}
} else {
if (name === 'webgl') {
document.body.removeChild((addon.instance as WebglAddon).textureAtlas);
}
addon.instance!.dispose();
addon.instance = undefined;
}
@@ -377,3 +402,14 @@ function updateTerminalSize(): void {
terminalContainer.style.height = height;
addons.fit.instance.fit();
}
function serializeButtonHandler(): void {
const output = addons.serialize.instance.serialize();
const outputString = JSON.stringify(output);
document.getElementById('serialize-output').innerText = outputString;
if ((document.getElementById('write-to-terminal') as HTMLInputElement).checked) {
term.reset();
term.write(output);
}
}
+15 -10
View File
@@ -11,16 +11,6 @@
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
<div id="terminal-container"></div>
<div>
<h3>Actions</h3>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p>
</div>
<div>
<h3>Options</h3>
<p>These options can be set in the <code>Terminal</code> constructor or using the <code>Terminal.setOption</code> function.</p>
@@ -30,6 +20,21 @@
<h3>Addons</h3>
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
<div id="addons-container"></div>
<h3>Addons Control</h3>
<h4>SearchAddon</h4>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
<label>Use regex<input type="checkbox" id="regex"/></label>
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
<label>Whole word<input type="checkbox" id="whole-word"/></label>
</p>
<h4>SerializeAddon</h4>
<p>
<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>
</p>
</div>
<div>
<h3>Style</h3>
+11
View File
@@ -30,3 +30,14 @@ p {
padding-left: 20px;
vertical-align: top;
}
pre {
display: block;
padding: 9.5px;
font-size: 13px;
color: #c7254e;
background-color: #f9f2f4;
word-break: break-all;
word-wrap: break-word;
white-space: pre-wrap;
}
+1
View File
@@ -9,6 +9,7 @@
"xterm-addon-attach": ["../addons/xterm-addon-attach"],
"xterm-addon-fit": ["../addons/xterm-addon-fit"],
"xterm-addon-search": ["../addons/xterm-addon-search"],
"xterm-addon-serialize": ["../addons/xterm-addon-serialize"],
"xterm-addon-web-links": ["../addons/xterm-addon-web-links"],
"xterm-addon-webgl": ["../addons/xterm-addon-webgl"]
}
+5 -3
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "4.3.0",
"version": "4.4.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
@@ -26,7 +26,8 @@
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/test/benchmark/*benchmark.js",
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/test/benchmark/*benchmark.js",
"clean": "rm -rf lib out addons/*/lib addons/*/out"
"clean": "rm -rf lib out addons/*/lib addons/*/out",
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts"
},
"devDependencies": {
"@types/chai": "^3.4.34",
@@ -46,7 +47,8 @@
"glob": "^7.0.5",
"jsdom": "^11.11.0",
"mocha": "^6.1.4",
"node-pty": "0.7.6",
"mustache": "^3.0.1",
"node-pty": "^0.9.0",
"nyc": "13",
"puppeteer": "^1.15.0",
"source-map-loader": "^0.2.4",
+194 -15
View File
@@ -7,13 +7,13 @@ import { assert, expect } from 'chai';
import { InputHandler } from './InputHandler';
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
import { Terminal } from './Terminal';
import { IBufferLine } from 'common/Types';
import { IBufferLine, IAttributeData } from 'common/Types';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { Attributes } from 'common/buffer/Constants';
import { AttributeData } from 'common/buffer/AttributeData';
import { Params } from 'common/parser/Params';
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService } from 'common/TestUtils.test';
import { MockCoreService, MockBufferService, MockDirtyRowService, MockOptionsService, MockLogService, MockCoreMouseService, MockCharsetService, MockUnicodeService } from 'common/TestUtils.test';
import { IBufferService } from 'common/services/Services';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { clone } from 'common/Clone';
@@ -33,34 +33,38 @@ function getLines(term: TestTerminal, limit: number = term.rows): string[] {
return res;
}
class TestInputHandler extends InputHandler {
get curAttrData(): IAttributeData { return (this as any)._curAttrData; }
}
describe('InputHandler', () => {
describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal();
terminal.curAttrData.fg = 3;
const bufferService = new MockBufferService(80, 30);
bufferService.buffer.x = 1;
bufferService.buffer.y = 2;
bufferService.buffer.ybase = 0;
const inputHandler = new InputHandler(terminal, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new TestInputHandler(terminal, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
inputHandler.curAttrData.fg = 3;
// Save cursor position
inputHandler.saveCursor();
assert.equal(bufferService.buffer.x, 1);
assert.equal(bufferService.buffer.y, 2);
assert.equal(terminal.curAttrData.fg, 3);
assert.equal(inputHandler.curAttrData.fg, 3);
// Change cursor position
bufferService.buffer.x = 10;
bufferService.buffer.y = 20;
terminal.curAttrData.fg = 30;
inputHandler.curAttrData.fg = 30;
// Restore cursor position
inputHandler.restoreCursor();
assert.equal(bufferService.buffer.x, 1);
assert.equal(bufferService.buffer.y, 2);
assert.equal(terminal.curAttrData.fg, 3);
assert.equal(inputHandler.curAttrData.fg, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
const optionsService = new MockOptionsService();
const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService());
const inputHandler = new InputHandler(new MockInputHandlingTerminal(), new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService(), {} as any);
inputHandler.setCursorStyle(Params.fromArray([0]));
assert.equal(optionsService.options['cursorStyle'], 'block');
@@ -101,7 +105,7 @@ describe('InputHandler', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
const terminal = new MockInputHandlingTerminal();
terminal.bracketedPasteMode = false;
const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(terminal, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
// Set bracketed paste mode
inputHandler.setModePrivate(Params.fromArray([2004]));
assert.equal(terminal.bracketedPasteMode, true);
@@ -120,7 +124,7 @@ describe('InputHandler', () => {
it('insertChars', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
// insert some data in first and second line
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
@@ -158,7 +162,7 @@ describe('InputHandler', () => {
it('deleteChars', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
// insert some data in first and second line
inputHandler.parse(Array(bufferService.cols - 9).join('a'));
@@ -199,7 +203,7 @@ describe('InputHandler', () => {
it('eraseInLine', function(): void {
const term = new Terminal();
const bufferService = new MockBufferService(80, 30);
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
// fill 6 lines to test 3 different states
inputHandler.parse(Array(bufferService.cols + 1).join('a'));
@@ -228,7 +232,7 @@ describe('InputHandler', () => {
it('eraseInDisplay', function(): void {
const term = new Terminal({cols: 80, rows: 7});
const bufferService = new MockBufferService(80, 7);
const inputHandler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
// fill display with a's
for (let i = 0; i < bufferService.rows; ++i) inputHandler.parse(Array(bufferService.cols + 1).join('a'));
@@ -363,7 +367,7 @@ describe('InputHandler', () => {
describe('print', () => {
it('should not cause an infinite loop (regression test)', () => {
const term = new Terminal();
const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
const inputHandler = new InputHandler(term, new MockBufferService(80, 30), new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
const container = new Uint32Array(10);
container[0] = 0x200B;
inputHandler.print(container, 0, 1);
@@ -378,7 +382,7 @@ describe('InputHandler', () => {
beforeEach(() => {
term = new Terminal();
bufferService = new MockBufferService(80, 30);
handler = new InputHandler(term, bufferService, new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService());
handler = new InputHandler(term, bufferService, new MockCharsetService(), new MockCoreService(), new MockDirtyRowService(), new MockLogService(), new MockOptionsService(), new MockCoreMouseService(), new MockUnicodeService(), {} as any);
});
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
@@ -1266,4 +1270,179 @@ describe('InputHandler', () => {
[131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072]
]);
});
describe('windowOptions', () => {
it('all should be disabled by default and not report', () => {
const term = new TestTerminal({cols: 10, rows: 10});
const stack: string[] = [];
term.onData(data => stack.push(data));
term.writeSync('\x1b[14t');
term.writeSync('\x1b[16t');
term.writeSync('\x1b[18t');
term.writeSync('\x1b[20t');
term.writeSync('\x1b[21t');
assert.deepEqual(stack, []);
});
it('14 - GetWinSizePixels', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {getWinSizePixels: true}});
const stack: string[] = [];
term.onData(data => stack.push(data));
term.writeSync('\x1b[14t');
// does not report in test terminal due to missing renderer
assert.deepEqual(stack, []);
});
it('16 - GetCellSizePixels', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {getCellSizePixels: true}});
const stack: string[] = [];
term.onData(data => stack.push(data));
term.writeSync('\x1b[16t');
// does not report in test terminal due to missing renderer
assert.deepEqual(stack, []);
});
it('18 - GetWinSizeChars', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {getWinSizeChars: true}});
const stack: string[] = [];
term.onData(data => stack.push(data));
term.writeSync('\x1b[18t');
assert.deepEqual(stack, ['\x1b[8;10;10t']);
term.resize(50, 20);
term.writeSync('\x1b[18t');
assert.deepEqual(stack, ['\x1b[8;10;10t', '\x1b[8;20;50t']);
});
it('22/23 - PushTitle/PopTitle', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {pushTitle: true, popTitle: true}});
const stack: string[] = [];
term.onTitleChange(data => stack.push(data));
term.writeSync('\x1b]0;1\x07');
term.writeSync('\x1b[22t');
term.writeSync('\x1b]0;2\x07');
term.writeSync('\x1b[22t');
term.writeSync('\x1b]0;3\x07');
term.writeSync('\x1b[22t');
assert.deepEqual((term as any)._inputHandler._windowTitleStack, ['1', '2', '3']);
assert.deepEqual((term as any)._inputHandler._iconNameStack, ['1', '2', '3']);
assert.deepEqual(stack, ['1', '2', '3']);
term.writeSync('\x1b[23t');
term.writeSync('\x1b[23t');
term.writeSync('\x1b[23t');
term.writeSync('\x1b[23t'); // one more to test "overflow"
assert.deepEqual((term as any)._inputHandler._windowTitleStack, []);
assert.deepEqual((term as any)._inputHandler._iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']);
});
it('22/23 - PushTitle/PopTitle with ;1', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {pushTitle: true, popTitle: true}});
const stack: string[] = [];
term.onTitleChange(data => stack.push(data));
term.writeSync('\x1b]0;1\x07');
term.writeSync('\x1b[22;1t');
term.writeSync('\x1b]0;2\x07');
term.writeSync('\x1b[22;1t');
term.writeSync('\x1b]0;3\x07');
term.writeSync('\x1b[22;1t');
assert.deepEqual((term as any)._inputHandler._windowTitleStack, []);
assert.deepEqual((term as any)._inputHandler._iconNameStack, ['1', '2', '3']);
assert.deepEqual(stack, ['1', '2', '3']);
term.writeSync('\x1b[23;1t');
term.writeSync('\x1b[23;1t');
term.writeSync('\x1b[23;1t');
term.writeSync('\x1b[23;1t'); // one more to test "overflow"
assert.deepEqual((term as any)._inputHandler._windowTitleStack, []);
assert.deepEqual((term as any)._inputHandler._iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3']);
});
it('22/23 - PushTitle/PopTitle with ;2', () => {
const term = new TestTerminal({cols: 10, rows: 10, windowOptions: {pushTitle: true, popTitle: true}});
const stack: string[] = [];
term.onTitleChange(data => stack.push(data));
term.writeSync('\x1b]0;1\x07');
term.writeSync('\x1b[22;2t');
term.writeSync('\x1b]0;2\x07');
term.writeSync('\x1b[22;2t');
term.writeSync('\x1b]0;3\x07');
term.writeSync('\x1b[22;2t');
assert.deepEqual((term as any)._inputHandler._windowTitleStack, ['1', '2', '3']);
assert.deepEqual((term as any)._inputHandler._iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3']);
term.writeSync('\x1b[23;2t');
term.writeSync('\x1b[23;2t');
term.writeSync('\x1b[23;2t');
term.writeSync('\x1b[23;2t'); // one more to test "overflow"
assert.deepEqual((term as any)._inputHandler._windowTitleStack, []);
assert.deepEqual((term as any)._inputHandler._iconNameStack, []);
assert.deepEqual(stack, ['1', '2', '3', '3', '2', '1']);
});
it('DECCOLM - should only work with "SetWinLines" (24) enabled', () => {
// disabled
const term = new TestTerminal({cols: 10, rows: 10});
term.writeSync('\x1b[?3l');
assert.equal((term as any)._bufferService.cols, 10);
term.writeSync('\x1b[?3h');
assert.equal((term as any)._bufferService.cols, 10);
// enabled
const term2 = new TestTerminal({cols: 10, rows: 10, windowOptions: {setWinLines: true}});
term2.writeSync('\x1b[?3l');
assert.equal((term2 as any)._bufferService.cols, 80);
term2.writeSync('\x1b[?3h');
assert.equal((term2 as any)._bufferService.cols, 132);
});
});
describe('should correctly reset cells taken by wide chars', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 5, scrollback: 1});
term.writeSync('¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥¥');
});
it('print', () => {
term.writeSync('\x1b[H#');
assert.deepEqual(getLines(term), ['# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[1;6H######');
assert.deepEqual(getLines(term), ['# ¥ #####', '# ¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('#');
assert.deepEqual(getLines(term), ['# ¥ #####', '##¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('#');
assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[3;9H#');
assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥#', '¥¥¥¥¥', '']);
term.writeSync('#');
assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '¥¥¥¥¥', '']);
term.writeSync('#');
assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥¥', '']);
term.writeSync('\x1b[4;10H#');
assert.deepEqual(getLines(term), ['# ¥ #####', '### ¥¥¥', '¥¥¥¥##', '# ¥¥¥ #', '']);
});
it('EL', () => {
term.writeSync('\x1b[1;6H\x1b[K#');
assert.deepEqual(getLines(term), ['¥¥ #', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[2;5H\x1b[1K');
assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[3;6H\x1b[1K');
assert.deepEqual(getLines(term), ['¥¥ #', ' ¥¥', ' ¥¥', '¥¥¥¥¥', '']);
});
it('ICH', () => {
term.writeSync('\x1b[1;6H\x1b[@');
assert.deepEqual(getLines(term), ['¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[2;4H\x1b[2@');
assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[3;4H\x1b[3@');
assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[4;4H\x1b[4@');
assert.deepEqual(getLines(term), ['¥¥ ¥', '¥ ¥¥', '¥ ¥', '¥ ¥', '']);
});
it('DCH', () => {
term.writeSync('\x1b[1;6H\x1b[P');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[2;6H\x1b[2P');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[3;6H\x1b[3P');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
});
it('ECH', () => {
term.writeSync('\x1b[1;6H\x1b[X');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[2;6H\x1b[2X');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥¥¥¥', '¥¥¥¥¥', '']);
term.writeSync('\x1b[3;6H\x1b[3X');
assert.deepEqual(getLines(term), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
});
});
});
+739 -213
View File
File diff suppressed because it is too large Load Diff
+61 -25
View File
@@ -7,15 +7,17 @@ import { assert, expect } from 'chai';
import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from './TestUtils.test';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { wcwidth } from 'common/CharWidth';
import { IBufferService } from 'common/services/Services';
import { IBufferService, IUnicodeService } from 'common/services/Services';
import { Linkifier } from 'browser/Linkifier';
import { MockLogService } from 'common/TestUtils.test';
import { MockLogService, MockOptionsService, MockUnicodeService } from 'common/TestUtils.test';
import { IRegisteredLinkMatcher, IMouseZoneManager, IMouseZone } from 'browser/Types';
const INIT_COLS = 80;
const INIT_ROWS = 24;
// grab wcwidth from mock unicode service (hardcoded to V6)
const wcwidth = (new MockUnicodeService()).wcwidth;
describe('Terminal', () => {
let term: TestTerminal;
const termOptions = {
@@ -112,7 +114,7 @@ describe('Terminal', () => {
assert.equal(typeof e, 'number');
done();
});
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
});
it('should fire the onTitleChange event', (done) => {
term.onTitleChange(e => {
@@ -397,7 +399,7 @@ describe('Terminal', () => {
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b');
@@ -410,7 +412,7 @@ describe('Terminal', () => {
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
@@ -424,7 +426,7 @@ describe('Terminal', () => {
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b');
@@ -443,7 +445,7 @@ describe('Terminal', () => {
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
@@ -465,7 +467,7 @@ describe('Terminal', () => {
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
assert.equal(term.buffer.lines.length, INIT_ROWS);
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
// 'a' gets pushed out of buffer
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
@@ -480,7 +482,7 @@ describe('Terminal', () => {
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
@@ -494,7 +496,7 @@ describe('Terminal', () => {
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
term.buffer.y = 3;
term.buffer.scrollBottom = 3;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
@@ -512,7 +514,7 @@ describe('Terminal', () => {
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
term.buffer.scrollTop = 1;
term.buffer.scrollBottom = 3;
term.scroll();
term.scroll(DEFAULT_ATTR_DATA.clone());
assert.equal(term.buffer.lines.length, INIT_ROWS);
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
@@ -747,7 +749,7 @@ describe('Terminal', () => {
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = true;
term.writeSync('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
@@ -761,7 +763,7 @@ describe('Terminal', () => {
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = false;
term.writeSync('\x1b[?7l'); // Disable wraparound mode
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
@@ -812,7 +814,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(1);
});
it('multiple combined é', () => {
term.wraparoundMode = true;
term.writeSync(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
@@ -826,7 +827,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(1);
});
it('multiple surrogate with combined', () => {
term.wraparoundMode = true;
term.writeSync(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
@@ -855,7 +855,6 @@ describe('Terminal', () => {
expect(term.buffer.x).eql(3);
});
it('line of ¥ even', () => {
term.wraparoundMode = true;
term.writeSync(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
@@ -875,7 +874,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(2);
});
it('line of ¥ odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.writeSync(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
@@ -900,7 +898,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(2);
});
it('line of ¥ with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.writeSync(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
@@ -925,7 +922,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(2);
});
it('line of ¥ with combining even', () => {
term.wraparoundMode = true;
term.writeSync(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
@@ -945,7 +941,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(2);
});
it('line of surrogate fullwidth with combining odd', () => {
term.wraparoundMode = true;
term.buffer.x = 1;
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
@@ -970,7 +965,6 @@ describe('Terminal', () => {
expect(cell.getWidth()).eql(2);
});
it('line of surrogate fullwidth with combining even', () => {
term.wraparoundMode = true;
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell);
@@ -1045,7 +1039,7 @@ describe('Terminal', () => {
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
beforeEach(() => {
terminal = new TestTerminal({ cols: 10, rows: 5 });
linkifier = new TestLinkifier((terminal as any)._bufferService);
linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService);
mouseZoneManager = new TestMouseZoneManager();
linkifier.attachToDom({} as any, mouseZoneManager);
});
@@ -1367,11 +1361,53 @@ describe('Terminal', () => {
}).to.not.throw();
});
});
describe('Windows Mode', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => {
const data = [
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
'aaaaaaaaa\n\r', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false);
});
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => {
const data = [
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
'aaaaaaaaa' // not wrapped
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0).isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1).isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2).isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0).isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1).isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2).isWrapped, false);
});
});
});
class TestLinkifier extends Linkifier {
constructor(bufferService: IBufferService) {
super(bufferService, new MockLogService());
constructor(bufferService: IBufferService, unicodeService: IUnicodeService) {
super(bufferService, new MockLogService(), new MockOptionsService(), unicodeService);
Linkifier._timeBeforeLatency = 0;
}
+48 -92
View File
@@ -39,21 +39,20 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { IKeyboardEvent, KeyboardResultType, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { handleWindowsModeLineFeed } from 'common/WindowsMode';
import { updateWindowsModeWrappedState } from 'common/WindowsMode';
import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService';
import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService, ICharsetService, IUnicodeService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { Disposable } from 'common/Lifecycle';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { Attributes } from 'common/buffer/Constants';
import { MouseService } from 'browser/services/MouseService';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { CoreService } from 'common/services/CoreService';
@@ -64,6 +63,8 @@ import { InstantiationService } from 'common/services/InstantiationService';
import { CoreMouseService } from 'common/services/CoreMouseService';
import { WriteBuffer } from 'common/input/WriteBuffer';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
import { UnicodeService } from 'common/services/UnicodeService';
import { CharsetService } from 'common/services/CharsetService';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -74,10 +75,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public element: HTMLElement;
public screenElement: HTMLElement;
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
*/
private _parent: HTMLElement | null;
private _document: Document;
private _viewportScrollArea: HTMLElement;
private _viewportElement: HTMLElement;
@@ -96,11 +93,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// common services
private _bufferService: IBufferService;
private _coreService: ICoreService;
private _charsetService: ICharsetService;
private _coreMouseService: ICoreMouseService;
private _dirtyRowService: IDirtyRowService;
private _instantiationService: IInstantiationService;
private _logService: ILogService;
public optionsService: IOptionsService;
public unicodeService: IUnicodeService;
// browser services
private _charSizeService: ICharSizeService;
@@ -110,32 +109,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
private _soundService: ISoundService;
// modes
public applicationKeypad: boolean;
public originMode: boolean;
public insertMode: boolean;
public wraparoundMode: boolean; // defaults: xterm - true, vt100 - false
public bracketedPasteMode: boolean;
// charset
// The current charset
public charset: ICharset;
public gcharset: number;
public glevel: number;
public charsets: ICharset[];
// mouse properties
public mouseEvents: CoreMouseEventType = CoreMouseEventType.NONE;
public sendFocus: boolean;
// misc
public savedCols: number;
public curAttrData: IAttributeData;
private _eraseAttrData: IAttributeData;
public params: (string | number)[];
public currentParam: string | number;
// write buffer
private _writeBuffer: WriteBuffer;
@@ -228,6 +208,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._instantiationService.setService(ICoreMouseService, this._coreMouseService);
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
this._instantiationService.setService(IDirtyRowService, this._dirtyRowService);
this.unicodeService = this._instantiationService.createInstance(UnicodeService);
this._instantiationService.setService(IUnicodeService, this.unicodeService);
this._charsetService = this._instantiationService.createInstance(CharsetService);
this._instantiationService.setService(ICharsetService, this._charsetService);
this._setupOptionsListeners();
this._setup();
@@ -249,39 +233,30 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
}
private _setup(): void {
this._parent = document ? document.body : null;
this._customKeyEventHandler = null;
// modes
this.applicationKeypad = false;
this.originMode = false;
this.insertMode = false;
this.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this.bracketedPasteMode = false;
// charset
this.charset = null;
this.gcharset = null;
this.glevel = 0;
// TODO: Can this be just []?
this.charsets = [null];
this.curAttrData = DEFAULT_ATTR_DATA.clone();
this._eraseAttrData = DEFAULT_ATTR_DATA.clone();
this.params = [];
this.currentParam = 0;
this._userScrolling = false;
// Register input handler and refire/handle events
this._inputHandler = new InputHandler(this, this._bufferService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService);
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
this.register(this._inputHandler);
if (this._inputHandler) {
this._inputHandler.reset();
} else {
// Register input handler and refire/handle events
this._inputHandler = new InputHandler(this, this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService, this._instantiationService);
this._inputHandler.onRequestBell(() => this.bell());
this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end));
this._inputHandler.onRequestReset(() => this.reset());
this._inputHandler.onCursorMove(() => this._onCursorMove.fire());
this._inputHandler.onLineFeed(() => this._onLineFeed.fire());
this.register(this._inputHandler);
}
this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService);
if (!this.linkifier) {
this.linkifier = new Linkifier(this._bufferService, this._logService, this.optionsService, this.unicodeService);
}
if (this.options.windowsMode) {
this._enableWindowsMode();
@@ -290,7 +265,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
private _enableWindowsMode(): void {
if (!this._windowsMode) {
this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService));
const disposables: IDisposable[] = [];
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
disposables.push(this.addCsiHandler({ final: 'H' }, () => {
updateWindowsModeWrappedState(this._bufferService);
return false;
}));
this._windowsMode = {
dispose: () => {
disposables.forEach(d => d.dispose());
}
};
}
}
@@ -305,15 +290,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
return this._bufferService.buffers;
}
/**
* back_color_erase feature for xterm.
*/
public eraseAttrData(): IAttributeData {
this._eraseAttrData.bg &= ~(Attributes.CM_MASK | 0xFFFFFF);
this._eraseAttrData.bg |= this.curAttrData.bg & ~0xFC000000;
return this._eraseAttrData;
}
/**
* Focus the terminal. Delegates focus handling to the terminal's DOM element.
*/
@@ -333,6 +309,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._renderService?.clear();
this._charSizeService?.measure();
break;
case 'cursorBlink':
case 'cursorStyle':
// The DOM renderer needs a row refresh to update the cursor styles
this.refresh(this.buffer.y, this.buffer.y);
break;
case 'drawBoldTextInBrightColors':
case 'letterSpacing':
case 'lineHeight':
@@ -485,9 +466,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* @param parent The element to create the terminal within.
*/
public open(parent: HTMLElement): void {
this._parent = parent || this._parent;
if (!this._parent) {
if (!parent) {
throw new Error('Terminal requires a parent element.');
}
@@ -495,7 +474,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._logService.warn('Terminal.open was called on an element that was not attached to the DOM');
}
this._document = this._parent.ownerDocument;
this._document = parent.ownerDocument;
// Create main element container
this.element = this._document.createElement('div');
@@ -503,7 +482,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.element.classList.add('terminal');
this.element.classList.add('xterm');
this.element.setAttribute('tabindex', '0');
this._parent.appendChild(this.element);
parent.appendChild(this.element);
// Performance: Use a document fragment to build the terminal
// viewport and helper elements detached from the DOM
@@ -958,10 +937,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* Scroll the terminal down 1 row, creating a blank line.
* @param isWrapped Whether the new line is wrapped from the previous line.
*/
public scroll(isWrapped: boolean = false): void {
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
let newLine: IBufferLine;
newLine = this._blankLine;
const eraseAttr = this.eraseAttrData();
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = this.buffer.getBlankLine(eraseAttr, isWrapped);
this._blankLine = newLine;
@@ -1306,27 +1284,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
/**
* Set the G level of the terminal
* @param g
*/
public setgLevel(g: number): void {
this.glevel = g;
this.charset = this.charsets[g];
}
/**
* Set the charset for the given G level of the terminal
* @param g
* @param charset
*/
public setgCharset(g: number, charset: ICharset): void {
this.charsets[g] = charset;
if (this.glevel === g) {
this.charset = charset;
}
}
protected _keyUp(ev: KeyboardEvent): void {
if (this._customKeyEventHandler && this._customKeyEventHandler(ev) === false) {
return;
@@ -1432,7 +1389,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
// invalid location
this.viewport.syncScrollArea(true);
this.viewport?.syncScrollArea(true);
this.refresh(0, this.rows - 1);
this._onResize.fire({ cols: x, rows: y });
@@ -1512,18 +1469,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.options.rows = this.rows;
this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler;
const userScrolling = this._userScrolling;
this._setup();
this._bufferService.reset();
this._charsetService.reset();
this._coreService.reset();
this._coreMouseService.reset();
this._selectionService?.reset();
// reattach
this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler;
this._userScrolling = userScrolling;
// do a full screen refresh
+5 -91
View File
@@ -6,19 +6,20 @@
import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset } from 'common/Types';
import { Buffer } from 'common/buffer/Buffer';
import * as Browser from 'common/Platform';
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
import { Terminal } from './Terminal';
import { AttributeData } from 'common/buffer/AttributeData';
import { IColorManager, IColorSet, ILinkMatcherOptions, ILinkifier, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IOptionsService, IUnicodeService } from 'common/services/Services';
import { EventEmitter } from 'common/EventEmitter';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { ISelectionService } from 'browser/services/Services';
export class TestTerminal extends Terminal {
get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
keyDown(ev: any): boolean { return this._keyDown(ev); }
keyPress(ev: any): boolean { return this._keyPress(ev); }
}
@@ -40,6 +41,7 @@ export class MockTerminal implements ITerminal {
onResize: IEvent<{ cols: number; rows: number; }>;
markers: IMarker[];
optionsService: IOptionsService;
unicodeService: IUnicodeService;
addMarker(cursorYOffset: number): IMarker {
throw new Error('Method not implemented.');
}
@@ -197,112 +199,24 @@ export class MockTerminal implements ITerminal {
export class MockInputHandlingTerminal implements IInputHandlingTerminal {
onA11yCharEmitter: EventEmitter<string>;
onA11yTabEmitter: EventEmitter<number>;
element: HTMLElement;
options: ITerminalOptions = {};
cols: number;
rows: number;
charset: { [key: string]: string; };
gcharset: number;
glevel: number;
charsets: { [key: string]: string; }[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttrData = new AttributeData();
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: CoreMouseEventType;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer = new MockBuffer();
viewport: IViewport;
selectionService: ISelectionService;
focus(): void {
throw new Error('Method not implemented.');
}
convertEol: boolean;
bell(): void {
throw new Error('Method not implemented.');
}
updateRange(y: number): void {
throw new Error('Method not implemented.');
}
scroll(isWrapped?: boolean): void {
throw new Error('Method not implemented.');
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
}
setgLevel(g: number): void {
throw new Error('Method not implemented.');
}
eraseAttrData(): IAttributeData {
throw new Error('Method not implemented.');
}
eraseRight(x: number, y: number): void {
throw new Error('Method not implemented.');
}
eraseLine(y: number): void {
throw new Error('Method not implemented.');
}
eraseLeft(x: number, y: number): void {
throw new Error('Method not implemented.');
}
prevStop(x?: number): number {
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void {
throw new Error('Method not implemented.');
}
is(term: string): boolean {
throw new Error('Method not implemented.');
}
setgCharset(g: number, charset: { [key: string]: string; }): void {
throw new Error('Method not implemented.');
}
resize(x: number, y: number): void {
throw new Error('Method not implemented.');
}
log(text: string, data?: any): void {
throw new Error('Method not implemented.');
}
reset(): void {
throw new Error('Method not implemented.');
}
showCursor(): void {
throw new Error('Method not implemented.');
}
refresh(start: number, end: number): void {
throw new Error('Method not implemented.');
}
matchColor(r1: number, g1: number, b1: number): number {
throw new Error('Method not implemented.');
}
error(text: string, data?: any): void {
throw new Error('Method not implemented.');
}
setOption(key: string, value: any): void {
(<any>this.options)[key] = value;
}
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
throw new Error('Method not implemented.');
}
addDisposableListener(type: string, handler: XtermListener): IDisposable {
throw new Error('Method not implemented.');
}
handler(data: string): void {
throw new Error('Method not implemented.');
}
+4 -27
View File
@@ -7,7 +7,7 @@ import { ITerminalOptions as IPublicTerminalOptions, IDisposable, IMarker, ISele
import { ICharset, IAttributeData, CharData, CoreMouseEventType } from 'common/Types';
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IColorSet, ILinkifier, ILinkMatcherOptions, IViewport } from 'browser/Types';
import { IOptionsService } from 'common/services/Services';
import { IOptionsService, IUnicodeService } from 'common/services/Services';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IParams, IFunctionIdentifier } from 'common/parser/Types';
@@ -21,22 +21,8 @@ export type LineData = CharData[];
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: ICharset;
gcharset: number;
glevel: number;
charsets: ICharset[];
applicationKeypad: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
curAttrData: IAttributeData;
savedCols: number;
mouseEvents: CoreMouseEventType;
sendFocus: boolean;
buffers: IBufferSet;
@@ -46,17 +32,10 @@ export interface IInputHandlingTerminal {
onA11yCharEmitter: IEventEmitter<string>;
onA11yTabEmitter: IEventEmitter<number>;
bell(): void;
focus(): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttrData(): IAttributeData;
scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void;
is(term: string): boolean;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
handleTitle(title: string): void;
}
@@ -141,7 +120,7 @@ export interface IInputHandler {
/** ESC D */ index(): void;
/** ESC H */ tabSet(): void;
/** ESC M */ reverseIndex(): void;
/** ESC c */ reset(): void;
/** ESC c */ fullReset(): void;
/** ESC n
ESC o
ESC |
@@ -160,6 +139,7 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
optionsService: IOptionsService;
// TODO: We should remove options once components adopt optionsService
options: ITerminalOptions;
unicodeService: IUnicodeService;
onBlur: IEvent<void>;
onFocus: IEvent<void>;
@@ -239,10 +219,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
[key: string]: any;
cancelEvents?: boolean;
convertEol?: boolean;
handler?: (data: string) => void;
screenKeys?: boolean;
termName?: string;
useFlowControl?: boolean;
}
export interface IBrowser {
+232 -219
View File
@@ -4,50 +4,243 @@
*/
import { assert } from 'chai';
import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba, opaque, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color';
import { channels, color, css, rgb, rgba, toPaddedHex, contrastRatio } from 'browser/Color';
describe('Color', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF });
describe('channels', () => {
describe('toCss', () => {
it('should convert an rgb array to css hex string', () => {
assert.equal(channels.toCss(0x00, 0x00, 0x00), '#000000');
assert.equal(channels.toCss(0x10, 0x10, 0x10), '#101010');
assert.equal(channels.toCss(0x20, 0x20, 0x20), '#202020');
assert.equal(channels.toCss(0x30, 0x30, 0x30), '#303030');
assert.equal(channels.toCss(0x40, 0x40, 0x40), '#404040');
assert.equal(channels.toCss(0x50, 0x50, 0x50), '#505050');
assert.equal(channels.toCss(0x60, 0x60, 0x60), '#606060');
assert.equal(channels.toCss(0x70, 0x70, 0x70), '#707070');
assert.equal(channels.toCss(0x80, 0x80, 0x80), '#808080');
assert.equal(channels.toCss(0x90, 0x90, 0x90), '#909090');
assert.equal(channels.toCss(0xa0, 0xa0, 0xa0), '#a0a0a0');
assert.equal(channels.toCss(0xb0, 0xb0, 0xb0), '#b0b0b0');
assert.equal(channels.toCss(0xc0, 0xc0, 0xc0), '#c0c0c0');
assert.equal(channels.toCss(0xd0, 0xd0, 0xd0), '#d0d0d0');
assert.equal(channels.toCss(0xe0, 0xe0, 0xe0), '#e0e0e0');
assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff');
});
});
describe('toRgba', () => {
it('should convert an rgb array to an rgba number', () => {
assert.equal(channels.toRgba(0x00, 0x00, 0x00), 0x000000FF);
assert.equal(channels.toRgba(0x10, 0x10, 0x10), 0x101010FF);
assert.equal(channels.toRgba(0x20, 0x20, 0x20), 0x202020FF);
assert.equal(channels.toRgba(0x30, 0x30, 0x30), 0x303030FF);
assert.equal(channels.toRgba(0x40, 0x40, 0x40), 0x404040FF);
assert.equal(channels.toRgba(0x50, 0x50, 0x50), 0x505050FF);
assert.equal(channels.toRgba(0x60, 0x60, 0x60), 0x606060FF);
assert.equal(channels.toRgba(0x70, 0x70, 0x70), 0x707070FF);
assert.equal(channels.toRgba(0x80, 0x80, 0x80), 0x808080FF);
assert.equal(channels.toRgba(0x90, 0x90, 0x90), 0x909090FF);
assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF);
assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF);
assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF);
assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF);
assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF);
assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF);
assert.equal(channels.toRgba(0xff, 0xff, 0xff), 0xffffffFF);
});
it('should convert an rgba array to an rgba number', () => {
assert.equal(channels.toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000);
assert.equal(channels.toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010);
assert.equal(channels.toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020);
assert.equal(channels.toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030);
assert.equal(channels.toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040);
assert.equal(channels.toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050);
assert.equal(channels.toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060);
assert.equal(channels.toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070);
assert.equal(channels.toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080);
assert.equal(channels.toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090);
assert.equal(channels.toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0);
assert.equal(channels.toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0);
assert.equal(channels.toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0);
assert.equal(channels.toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0);
assert.equal(channels.toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0);
assert.equal(channels.toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0);
assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
});
describe('fromCss', () => {
it('should covert a CSS string to an IColor', () => {
assert.deepEqual(fromCss('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(fromCss('#101010'), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(fromCss('#202020'), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(fromCss('#303030'), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(fromCss('#404040'), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(fromCss('#505050'), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(fromCss('#606060'), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(fromCss('#707070'), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(fromCss('#808080'), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(fromCss('#909090'), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(fromCss('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(fromCss('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(fromCss('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(fromCss('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(fromCss('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(fromCss('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(fromCss('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
describe('color', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF });
assert.deepEqual(color.blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF });
});
});
describe('opaque', () => {
it('should make the color opaque', () => {
assert.deepEqual(color.opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(color.opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(color.opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(color.opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(color.opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(color.opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(color.opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(color.opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(color.opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(color.opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(color.opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(color.opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(color.opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(color.opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(color.opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(color.opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(color.opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF });
});
});
});
describe('css', () => {
describe('toColor', () => {
it('should covert a CSS string to an IColor', () => {
assert.deepEqual(css.toColor('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(css.toColor('#101010'), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(css.toColor('#202020'), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(css.toColor('#303030'), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(css.toColor('#404040'), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(css.toColor('#505050'), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(css.toColor('#606060'), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(css.toColor('#707070'), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(css.toColor('#808080'), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(css.toColor('#909090'), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(css.toColor('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(css.toColor('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(css.toColor('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(css.toColor('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(css.toColor('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(css.toColor('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(css.toColor('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
});
});
});
describe('rgb', () => {
describe('relativeLuminance', () => {
it('should calculate the relative luminance of the color', () => {
assert.equal(rgb.relativeLuminance(0x000000), 0);
assert.equal(rgb.relativeLuminance(0x101010).toFixed(4), '0.0052');
assert.equal(rgb.relativeLuminance(0x202020).toFixed(4), '0.0144');
assert.equal(rgb.relativeLuminance(0x303030).toFixed(4), '0.0296');
assert.equal(rgb.relativeLuminance(0x404040).toFixed(4), '0.0513');
assert.equal(rgb.relativeLuminance(0x505050).toFixed(4), '0.0802');
assert.equal(rgb.relativeLuminance(0x606060).toFixed(4), '0.1170');
assert.equal(rgb.relativeLuminance(0x707070).toFixed(4), '0.1620');
assert.equal(rgb.relativeLuminance(0x808080).toFixed(4), '0.2159');
assert.equal(rgb.relativeLuminance(0x909090).toFixed(4), '0.2789');
assert.equal(rgb.relativeLuminance(0xA0A0A0).toFixed(4), '0.3515');
assert.equal(rgb.relativeLuminance(0xB0B0B0).toFixed(4), '0.4342');
assert.equal(rgb.relativeLuminance(0xC0C0C0).toFixed(4), '0.5271');
assert.equal(rgb.relativeLuminance(0xD0D0D0).toFixed(4), '0.6308');
assert.equal(rgb.relativeLuminance(0xE0E0E0).toFixed(4), '0.7454');
assert.equal(rgb.relativeLuminance(0xF0F0F0).toFixed(4), '0.8714');
assert.equal(rgb.relativeLuminance(0xFFFFFF), 1);
});
});
});
describe('rgba', () => {
describe('ensureContrastRatio', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 2), undefined);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 3), undefined);
});
it('should return a color that meets the contrast ratio (black bg)', () => {
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 4), 0x707070ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 5), 0x7f7f7fff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 6), 0x8c8c8cff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 7), 0x989898ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 9), 0xadadadff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 11), 0xbebebeff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 15), 0xdbdbdbff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 18), 0xeeeeeeff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 20), 0xfafafaff);
assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 21), 0xffffffff);
});
it('should return undefined if the color already meets the contrast ratio (white bg)', () => {
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 1), undefined);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 2), undefined);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 3), undefined);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 4), undefined);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 5), undefined);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 6), undefined);
});
it('should return a color that meets the contrast ratio (white bg)', () => {
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 7), 0x565656ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 8), 0x4d4d4dff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 9), 0x454545ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 10), 0x3e3e3eff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 11), 0x373737ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 12), 0x313131ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 13), 0x313131ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 14), 0x272727ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 15), 0x232323ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 16), 0x1f1f1fff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 17), 0x1b1b1bff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 18), 0x151515ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 19), 0x101010ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 20), 0x080808ff);
assert.equal(rgba.ensureContrastRatio(0xffffffff, 0x606060ff, 21), 0x000000ff);
});
});
describe('toChannels', () => {
it('should convert an rgba number to an rgba array', () => {
assert.deepEqual(rgba.toChannels(0x00000000), [0x00, 0x00, 0x00, 0x00]);
assert.deepEqual(rgba.toChannels(0x10101010), [0x10, 0x10, 0x10, 0x10]);
assert.deepEqual(rgba.toChannels(0x20202020), [0x20, 0x20, 0x20, 0x20]);
assert.deepEqual(rgba.toChannels(0x30303030), [0x30, 0x30, 0x30, 0x30]);
assert.deepEqual(rgba.toChannels(0x40404040), [0x40, 0x40, 0x40, 0x40]);
assert.deepEqual(rgba.toChannels(0x50505050), [0x50, 0x50, 0x50, 0x50]);
assert.deepEqual(rgba.toChannels(0x60606060), [0x60, 0x60, 0x60, 0x60]);
assert.deepEqual(rgba.toChannels(0x70707070), [0x70, 0x70, 0x70, 0x70]);
assert.deepEqual(rgba.toChannels(0x80808080), [0x80, 0x80, 0x80, 0x80]);
assert.deepEqual(rgba.toChannels(0x90909090), [0x90, 0x90, 0x90, 0x90]);
assert.deepEqual(rgba.toChannels(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]);
assert.deepEqual(rgba.toChannels(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]);
assert.deepEqual(rgba.toChannels(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]);
assert.deepEqual(rgba.toChannels(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]);
assert.deepEqual(rgba.toChannels(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]);
assert.deepEqual(rgba.toChannels(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]);
assert.deepEqual(rgba.toChannels(0xffffffff), [0xff, 0xff, 0xff, 0xff]);
});
});
});
@@ -73,134 +266,6 @@ describe('Color', () => {
});
});
describe('toCss', () => {
it('should convert an rgb array to css hex string', () => {
assert.equal(toCss(0x00, 0x00, 0x00), '#000000');
assert.equal(toCss(0x10, 0x10, 0x10), '#101010');
assert.equal(toCss(0x20, 0x20, 0x20), '#202020');
assert.equal(toCss(0x30, 0x30, 0x30), '#303030');
assert.equal(toCss(0x40, 0x40, 0x40), '#404040');
assert.equal(toCss(0x50, 0x50, 0x50), '#505050');
assert.equal(toCss(0x60, 0x60, 0x60), '#606060');
assert.equal(toCss(0x70, 0x70, 0x70), '#707070');
assert.equal(toCss(0x80, 0x80, 0x80), '#808080');
assert.equal(toCss(0x90, 0x90, 0x90), '#909090');
assert.equal(toCss(0xa0, 0xa0, 0xa0), '#a0a0a0');
assert.equal(toCss(0xb0, 0xb0, 0xb0), '#b0b0b0');
assert.equal(toCss(0xc0, 0xc0, 0xc0), '#c0c0c0');
assert.equal(toCss(0xd0, 0xd0, 0xd0), '#d0d0d0');
assert.equal(toCss(0xe0, 0xe0, 0xe0), '#e0e0e0');
assert.equal(toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(toCss(0xff, 0xff, 0xff), '#ffffff');
});
});
describe('toRgba', () => {
it('should convert an rgb array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00), 0x000000FF);
assert.equal(toRgba(0x10, 0x10, 0x10), 0x101010FF);
assert.equal(toRgba(0x20, 0x20, 0x20), 0x202020FF);
assert.equal(toRgba(0x30, 0x30, 0x30), 0x303030FF);
assert.equal(toRgba(0x40, 0x40, 0x40), 0x404040FF);
assert.equal(toRgba(0x50, 0x50, 0x50), 0x505050FF);
assert.equal(toRgba(0x60, 0x60, 0x60), 0x606060FF);
assert.equal(toRgba(0x70, 0x70, 0x70), 0x707070FF);
assert.equal(toRgba(0x80, 0x80, 0x80), 0x808080FF);
assert.equal(toRgba(0x90, 0x90, 0x90), 0x909090FF);
assert.equal(toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF);
assert.equal(toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF);
assert.equal(toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF);
assert.equal(toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF);
assert.equal(toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF);
assert.equal(toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF);
assert.equal(toRgba(0xff, 0xff, 0xff), 0xffffffFF);
});
it('should convert an rgba array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000);
assert.equal(toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010);
assert.equal(toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020);
assert.equal(toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030);
assert.equal(toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040);
assert.equal(toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050);
assert.equal(toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060);
assert.equal(toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070);
assert.equal(toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080);
assert.equal(toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090);
assert.equal(toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0);
assert.equal(toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0);
assert.equal(toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0);
assert.equal(toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0);
assert.equal(toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0);
assert.equal(toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0);
assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
describe('fromRgba', () => {
it('should convert an rgba number to an rgba array', () => {
assert.deepEqual(fromRgba(0x00000000), [0x00, 0x00, 0x00, 0x00]);
assert.deepEqual(fromRgba(0x10101010), [0x10, 0x10, 0x10, 0x10]);
assert.deepEqual(fromRgba(0x20202020), [0x20, 0x20, 0x20, 0x20]);
assert.deepEqual(fromRgba(0x30303030), [0x30, 0x30, 0x30, 0x30]);
assert.deepEqual(fromRgba(0x40404040), [0x40, 0x40, 0x40, 0x40]);
assert.deepEqual(fromRgba(0x50505050), [0x50, 0x50, 0x50, 0x50]);
assert.deepEqual(fromRgba(0x60606060), [0x60, 0x60, 0x60, 0x60]);
assert.deepEqual(fromRgba(0x70707070), [0x70, 0x70, 0x70, 0x70]);
assert.deepEqual(fromRgba(0x80808080), [0x80, 0x80, 0x80, 0x80]);
assert.deepEqual(fromRgba(0x90909090), [0x90, 0x90, 0x90, 0x90]);
assert.deepEqual(fromRgba(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]);
assert.deepEqual(fromRgba(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]);
assert.deepEqual(fromRgba(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]);
assert.deepEqual(fromRgba(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]);
assert.deepEqual(fromRgba(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]);
assert.deepEqual(fromRgba(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]);
assert.deepEqual(fromRgba(0xffffffff), [0xff, 0xff, 0xff, 0xff]);
});
});
describe('opaque', () => {
it('should make the color opaque', () => {
assert.deepEqual(opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF });
});
});
describe('rgbRelativeLuminance', () => {
it('should calculate the relative luminance of the color', () => {
assert.equal(rgbRelativeLuminance(0x000000), 0);
assert.equal(rgbRelativeLuminance(0x101010).toFixed(4), '0.0052');
assert.equal(rgbRelativeLuminance(0x202020).toFixed(4), '0.0144');
assert.equal(rgbRelativeLuminance(0x303030).toFixed(4), '0.0296');
assert.equal(rgbRelativeLuminance(0x404040).toFixed(4), '0.0513');
assert.equal(rgbRelativeLuminance(0x505050).toFixed(4), '0.0802');
assert.equal(rgbRelativeLuminance(0x606060).toFixed(4), '0.1170');
assert.equal(rgbRelativeLuminance(0x707070).toFixed(4), '0.1620');
assert.equal(rgbRelativeLuminance(0x808080).toFixed(4), '0.2159');
assert.equal(rgbRelativeLuminance(0x909090).toFixed(4), '0.2789');
assert.equal(rgbRelativeLuminance(0xA0A0A0).toFixed(4), '0.3515');
assert.equal(rgbRelativeLuminance(0xB0B0B0).toFixed(4), '0.4342');
assert.equal(rgbRelativeLuminance(0xC0C0C0).toFixed(4), '0.5271');
assert.equal(rgbRelativeLuminance(0xD0D0D0).toFixed(4), '0.6308');
assert.equal(rgbRelativeLuminance(0xE0E0E0).toFixed(4), '0.7454');
assert.equal(rgbRelativeLuminance(0xF0F0F0).toFixed(4), '0.8714');
assert.equal(rgbRelativeLuminance(0xFFFFFF), 1);
});
});
describe('contrastRatio', () => {
it('should calculate the relative luminance of the color', () => {
assert.equal(contrastRatio(0, 0), 1);
@@ -212,56 +277,4 @@ describe('Color', () => {
assert.equal(contrastRatio(1, 0), 21);
});
});
describe('ensureContrastRatioRgba', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 1), undefined);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 2), undefined);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 3), undefined);
});
it('should return a color that meets the contrast ratio (black bg)', () => {
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 4), 0x707070ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 5), 0x7f7f7fff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 6), 0x8c8c8cff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 7), 0x989898ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 9), 0xadadadff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 11), 0xbebebeff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 15), 0xdbdbdbff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 18), 0xeeeeeeff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 20), 0xfafafaff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 21), 0xffffffff);
});
it('should return undefined if the color already meets the contrast ratio (white bg)', () => {
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 1), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 2), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 3), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 4), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 5), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 6), undefined);
});
it('should return a color that meets the contrast ratio (white bg)', () => {
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 7), 0x565656ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 8), 0x4d4d4dff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 9), 0x454545ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 10), 0x3e3e3eff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 11), 0x373737ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 12), 0x313131ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 13), 0x313131ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 14), 0x272727ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 15), 0x232323ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 16), 0x1f1f1fff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 17), 0x1b1b1bff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 18), 0x151515ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 19), 0x101010ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 20), 0x080808ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 21), 0x000000ff);
});
});
});
+177 -152
View File
@@ -5,33 +5,186 @@
import { IColor } from 'browser/Types';
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
return {
css: fg.css,
rgba: fg.rgba
};
/**
* Helper functions where the source type is "channels" (individual color channels as numbers).
*/
export namespace channels {
export function toCss(r: number, g: number, b: number, a?: number): string {
if (a !== undefined) {
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
}
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
const fgR = (fg.rgba >> 24) & 0xFF;
const fgG = (fg.rgba >> 16) & 0xFF;
const fgB = (fg.rgba >> 8) & 0xFF;
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = toCss(r, g, b);
const rgba = toRgba(r, g, b);
return { css, rgba };
}
export function fromCss(css: string): IColor {
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
/**
* Helper functions where the source type is `IColor`.
*/
export namespace color {
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
return {
css: fg.css,
rgba: fg.rgba
};
}
const fgR = (fg.rgba >> 24) & 0xFF;
const fgG = (fg.rgba >> 16) & 0xFF;
const fgB = (fg.rgba >> 8) & 0xFF;
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = channels.toCss(r, g, b);
const rgba = channels.toRgba(r, g, b);
return { css, rgba };
}
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = rgba.ensureContrastRatio(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgba.toColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
export function opaque(color: IColor): IColor {
const rgbaColor = (color.rgba | 0xFF) >>> 0;
const [r, g, b] = rgba.toChannels(rgbaColor);
return {
css: channels.toCss(r, g, b),
rgba: rgbaColor
};
}
}
/**
* Helper functions where the source type is "css" (string: '#rgb', '#rgba', '#rrggbb', '#rrggbbaa').
*/
export namespace css {
export function toColor(css: string): IColor {
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
}
}
/**
* Helper functions where the source type is "rgb" (number: 0xrrggbb).
*/
export namespace rgb {
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param rgb The color to use.
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function relativeLuminance(rgb: number): number {
return relativeLuminance2(
(rgb >> 16) & 0xFF,
(rgb >> 8 ) & 0xFF,
(rgb ) & 0xFF);
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param r The red channel (0x00 to 0xFF).
* @param g The green channel (0x00 to 0xFF).
* @param b The blue channel (0x00 to 0xFF).
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function relativeLuminance2(r: number, g: number, b: number): number {
const rs = r / 255;
const gs = g / 255;
const bs = b / 255;
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
}
}
/**
* Helper functions where the source type is "rgba" (number: 0xrrggbbaa).
*/
export namespace rgba {
export function ensureContrastRatio(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgb.relativeLuminance(bgRgba >> 8);
const fgL = rgb.relativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bgRgba, fgRgba, ratio);
}
return increaseLuminance(bgRgba, fgRgba, ratio);
}
return undefined;
}
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to reducing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Reduce by 10% until the ratio is hit
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to increasing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
// Increase by 10% until the ratio is hit
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
cr = contrastRatio(rgb.relativeLuminance2(fgR, fgB, fgG), rgb.relativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function toChannels(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function toColor(r: number, g: number, b: number): IColor {
return {
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
};
}
}
export function toPaddedHex(c: number): string {
@@ -39,62 +192,6 @@ export function toPaddedHex(c: number): string {
return s.length < 2 ? '0' + s : s;
}
export function toCss(r: number, g: number, b: number, a?: number): string {
if (a !== undefined) {
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
}
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
export function fromRgba(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function opaque(color: IColor): IColor {
const rgba = (color.rgba | 0xFF) >>> 0;
const [r, g, b] = fromRgba(rgba);
return {
css: toCss(r, g, b),
rgba
};
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param rgb The color to use.
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function rgbRelativeLuminance(rgb: number): number {
return rgbRelativeLuminance2(
(rgb >> 16) & 0xFF,
(rgb >> 8 ) & 0xFF,
(rgb ) & 0xFF);
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param r The red channel (0x00 to 0xFF).
* @param g The green channel (0x00 to 0xFF).
* @param b The blue channel (0x00 to 0xFF).
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function rgbRelativeLuminance2(r: number, g: number, b: number): number {
const rs = r / 255;
const gs = g / 255;
const bs = b / 255;
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
}
/**
* Gets the contrast ratio between two relative luminance values.
* @param l1 The first relative luminance.
@@ -107,75 +204,3 @@ export function contrastRatio(l1: number, l2: number): number {
}
return (l1 + 0.05) / (l2 + 0.05);
}
export function rgbaToColor(r: number, g: number, b: number): IColor {
return {
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
};
}
export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgbRelativeLuminance(bgRgba >> 8);
const fgL = rgbRelativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bgRgba, fgRgba, ratio);
}
return increaseLuminance(bgRgba, fgRgba, ratio);
}
return undefined;
}
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgbaToColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to reducing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Reduce by 10% until the ratio is hit
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to increasing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
// Increase by 10% until the ratio is hit
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
+37 -49
View File
@@ -5,13 +5,13 @@
import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services';
import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color';
import { channels, color, css } from 'browser/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
const DEFAULT_FOREGROUND = fromCss('#ffffff');
const DEFAULT_BACKGROUND = fromCss('#000000');
const DEFAULT_CURSOR = fromCss('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromCss('#000000');
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
const DEFAULT_BACKGROUND = css.toColor('#000000');
const DEFAULT_CURSOR = css.toColor('#ffffff');
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF4D
@@ -22,23 +22,23 @@ const DEFAULT_SELECTION = {
export const DEFAULT_ANSI_COLORS = (() => {
const colors = [
// dark:
fromCss('#2e3436'),
fromCss('#cc0000'),
fromCss('#4e9a06'),
fromCss('#c4a000'),
fromCss('#3465a4'),
fromCss('#75507b'),
fromCss('#06989a'),
fromCss('#d3d7cf'),
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
fromCss('#555753'),
fromCss('#ef2929'),
fromCss('#8ae234'),
fromCss('#fce94f'),
fromCss('#729fcf'),
fromCss('#ad7fa8'),
fromCss('#34e2e2'),
fromCss('#eeeeec')
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
@@ -49,8 +49,8 @@ export const DEFAULT_ANSI_COLORS = (() => {
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
css: channels.toCss(r, g, b),
rgba: channels.toRgba(r, g, b)
});
}
@@ -58,8 +58,8 @@ export const DEFAULT_ANSI_COLORS = (() => {
for (let i = 0; i < 24; i++) {
const c = 8 + i * 10;
colors.push({
css: toCss(c, c, c),
rgba: toRgba(c, c, c)
css: channels.toCss(c, c, c),
rgba: channels.toRgba(c, c, c)
});
}
@@ -93,7 +93,7 @@ export class ColorManager implements IColorManager {
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
selectionOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
};
@@ -116,7 +116,7 @@ export class ColorManager implements IColorManager {
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection);
this.colors.selectionOpaque = color.blend(this.colors.background, this.colors.selection);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
@@ -184,35 +184,23 @@ export class ColorManager implements IColorManager {
);
return fallback;
}
let r: number;
let g: number;
let b: number;
let a: number;
let rgba: number;
if (css.length === 5) {
const num = parseInt(css.substr(1), 16);
r = ((num >> 12) & 0xF) * 16;
g = ((num >> 8) & 0xF) * 16;
b = ((num >> 4) & 0xF) * 16;
a = (num & 0xF) * 16;
rgba = toRgba(r, g, b, a);
} else {
rgba = parseInt(css.substr(1), 16);
r = (rgba >> 24) & 0xFF;
g = (rgba >> 16) & 0xFF;
b = (rgba >> 8) & 0xFF;
a = (rgba ) & 0xFF;
}
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// the color value has alpha less than 1.0, and the string is the color value in the CSS rgba()
const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component));
const alpha = Math.round(a * 255);
const rgba: number = channels.toRgba(r, g, b, alpha);
return {
rgba,
css: toCss(r, g, b, a)
css: channels.toCss(r, g, b, alpha)
};
}
return {
css,
rgba: toRgba(data[0], data[1], data[2], data[3])
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
// if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character
css: this._ctx.fillStyle,
rgba: channels.toRgba(data[0], data[1], data[2], data[3])
};
}
}
+3 -2
View File
@@ -17,12 +17,13 @@ export function addDisposableDomListener(
useCapture?: boolean
): IDisposable {
node.addEventListener(type, handler, useCapture);
let disposed = false;
return {
dispose: () => {
if (!handler) {
// Already disposed
if (!disposed) {
return;
}
disposed = true;
node.removeEventListener(type, handler, useCapture);
}
};
+3 -2
View File
@@ -9,12 +9,13 @@ import { IBufferLine } from 'common/Types';
import { Linkifier } from 'browser/Linkifier';
import { BufferLine } from 'common/buffer/BufferLine';
import { CellData } from 'common/buffer/CellData';
import { MockLogService, MockBufferService } from 'common/TestUtils.test';
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
import { IBufferService } from 'common/services/Services';
import { UnicodeService } from 'common/services/UnicodeService';
class TestLinkifier extends Linkifier {
constructor(bufferService: IBufferService) {
super(bufferService, new MockLogService());
super(bufferService, new MockLogService(), new MockOptionsService(), new UnicodeService());
Linkifier._timeBeforeLatency = 0;
}
+13 -6
View File
@@ -5,9 +5,8 @@
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types';
import { IBufferStringIteratorResult } from 'common/buffer/Types';
import { getStringCellWidth } from 'common/CharWidth';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { ILogService, IBufferService } from 'common/services/Services';
import { ILogService, IBufferService, IOptionsService, IUnicodeService } from 'common/services/Services';
/**
* Limit of the unwrapping line expansion (overscan) at the top and bottom
@@ -45,7 +44,9 @@ export class Linkifier implements ILinkifier {
constructor(
protected readonly _bufferService: IBufferService,
private readonly _logService: ILogService
private readonly _logService: ILogService,
private readonly _optionsService: IOptionsService,
private readonly _unicodeService: IUnicodeService
) {
this._rowsToLinkify = {
start: undefined,
@@ -277,8 +278,8 @@ export class Linkifier implements ILinkifier {
if (!this._mouseZoneManager || !this._element) {
return;
}
const width = getStringCellWidth(uri);
// FIXME: get cell length from buffer to avoid mismatch after Unicode version change
const width = this._unicodeService.getStringCellWidth(uri);
const x1 = x % this._bufferService.cols;
const y1 = y + Math.floor(x / this._bufferService.cols);
let x2 = (x1 + width) % this._bufferService.cols;
@@ -297,7 +298,13 @@ export class Linkifier implements ILinkifier {
if (matcher.handler) {
return matcher.handler(e, uri);
}
window.open(uri, '_blank');
const newWindow = window.open();
if (newWindow) {
newWindow.opener = null;
newWindow.location.href = uri;
} else {
console.warn('Opening link blocked as opener could not be cleared');
}
},
() => {
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
+29 -8
View File
@@ -19,19 +19,40 @@ describe('MoveToCell', () => {
describe('normal buffer', () => {
it('should use the right directional escape sequences', () => {
assert.equal(moveToCellSequence(1, 3, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(2, 3, bufferService, false), '\x1b[D');
assert.equal(moveToCellSequence(4, 3, bufferService, false), '\x1b[C');
assert.equal(moveToCellSequence(5, 3, bufferService, false), '\x1b[C\x1b[C');
});
it('should ignore the Y value', () => {
assert.equal(moveToCellSequence(1, 1, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 2, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 3, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 4, bufferService, false), '\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 5, bufferService, false), '\x1b[D\x1b[D');
it('should wrap around entire row instead of doing up and down when the Y value differs', () => {
assert.equal(moveToCellSequence(1, 1, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(2, 1, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(3, 1, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(4, 1, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(5, 1, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 2, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(2, 2, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(3, 2, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(4, 2, bufferService, false), '\x1b[D\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(5, 2, bufferService, false), '\x1b[D\x1b[D\x1b[D');
assert.equal(moveToCellSequence(1, 4, bufferService, false), '\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(2, 4, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(3, 4, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(4, 4, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(5, 4, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(1, 5, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(2, 5, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(3, 5, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(4, 5, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
assert.equal(moveToCellSequence(5, 5, bufferService, false), '\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C\x1b[C');
});
it('should use the correct character for application cursor', () => {
assert.equal(moveToCellSequence(2, 1, bufferService, false), '\x1b[D');
assert.equal(moveToCellSequence(2, 1, bufferService, true), '\x1bOD');
assert.equal(moveToCellSequence(3, 1, bufferService, true), '\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD');
assert.equal(moveToCellSequence(3, 2, bufferService, true), '\x1bOD\x1bOD\x1bOD\x1bOD\x1bOD');
assert.equal(moveToCellSequence(2, 3, bufferService, true), '\x1bOD');
assert.equal(moveToCellSequence(4, 3, bufferService, true), '\x1bOC');
assert.equal(moveToCellSequence(3, 4, bufferService, true), '\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC');
assert.equal(moveToCellSequence(3, 5, bufferService, true), '\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC\x1bOC');
});
});
+25 -1
View File
@@ -30,7 +30,31 @@ export function moveToCellSequence(targetX: number, targetY: number, bufferServi
}
// Only move horizontally for the normal buffer
return moveHorizontallyOnly(startX, startY, targetX, targetY, bufferService, applicationCursor);
let direction;
if (startY === targetY) {
direction = startX > targetX ? Direction.LEFT : Direction.RIGHT;
return repeat(Math.abs(startX - targetX), sequence(direction, applicationCursor));
}
direction = startY > targetY ? Direction.LEFT : Direction.RIGHT;
const rowDifference = Math.abs(startY - targetY);
const cellsToMove = colsFromRowEnd(startY > targetY ? targetX : startX, bufferService) +
(rowDifference - 1) * bufferService.cols + 1 /*wrap around 1 row*/ +
colsFromRowBeginning(startY > targetY ? startX : targetX, bufferService);
return repeat(cellsToMove, sequence(direction, applicationCursor));
}
/**
* Find the number of cols from a row beginning to a col.
*/
function colsFromRowBeginning(currX: number, bufferService: IBufferService): number {
return currX - 1;
}
/**
* Find the number of cols from a col to row end.
*/
function colsFromRowEnd(currX: number, bufferService: IBufferService): number {
return bufferService.cols - currX;
}
/**
+6 -6
View File
@@ -15,7 +15,7 @@ import { IColorSet, IColor } from 'browser/Types';
import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService } from 'common/services/Services';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { toCss, ensureContrastRatioRgba, opaque } from 'browser/Color';
import { channels, color, rgba } from 'browser/Color';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -171,11 +171,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to fill.
* @param y The row to fill.
*/
protected _fillLeftLineAtCell(x: number, y: number): void {
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
window.devicePixelRatio,
window.devicePixelRatio * width,
this._scaledCellHeight);
}
@@ -325,7 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isBgDefault()) {
this._ctx.fillStyle = opaque(this._colors.background).css;
this._ctx.fillStyle = color.opaque(this._colors.background).css;
} else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else {
@@ -418,7 +418,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
if (!result) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
@@ -426,7 +426,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
}
const color: IColor = {
css: toCss(
css: channels.toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
+1 -1
View File
@@ -209,7 +209,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._fillLeftLineAtCell(x, y, this._optionsService.options.cursorWidth);
this._ctx.restore();
}
@@ -11,7 +11,7 @@ import { LRUMap } from 'browser/renderer/atlas/LRUMap';
import { isFirefox, isSafari } from 'common/Platform';
import { IColor } from 'browser/Types';
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { opaque } from 'browser/Color';
import { color } from 'browser/Color';
// 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.
@@ -223,7 +223,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
return opaque(this._config.colors.background);
return color.opaque(this._config.colors.background);
} else if (glyph.fg < 256) {
// 256 color support
return this._getColorFromAnsiIndex(glyph.fg);
+7 -7
View File
@@ -11,7 +11,7 @@ import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService, IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { opaque } from 'browser/Color';
import { color } from 'browser/Color';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -173,13 +173,13 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`;
// Blink animation
styles +=
`@keyframes blink_box_shadow {` +
`@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` +
` 50% {` +
` box-shadow: none;` +
` }` +
`}`;
styles +=
`@keyframes blink_block {` +
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
` 0% {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
@@ -196,17 +196,17 @@ export class DomRenderer extends Disposable implements IRenderer {
` outline-offset: -1px;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
` animation: blink_box_shadow 1s step-end infinite;` +
` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` animation: blink_block 1s step-end infinite;` +
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
` box-shadow: ${this._optionsService.options.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;` +
@@ -231,7 +231,7 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
});
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${opaque(this._colors.background).css}; }` +
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
this._themeStyleElement.innerHTML = styles;
@@ -11,7 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test';
import { fromCss } from 'browser/Color';
import { css } from 'browser/Color';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
@@ -21,27 +21,27 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
background: fromCss('#010101'),
foreground: fromCss('#020202'),
background: css.toColor('#010101'),
foreground: css.toColor('#020202'),
ansi: [
// dark:
fromCss('#2e3436'),
fromCss('#cc0000'),
fromCss('#4e9a06'),
fromCss('#c4a000'),
fromCss('#3465a4'),
fromCss('#75507b'),
fromCss('#06989a'),
fromCss('#d3d7cf'),
css.toColor('#2e3436'),
css.toColor('#cc0000'),
css.toColor('#4e9a06'),
css.toColor('#c4a000'),
css.toColor('#3465a4'),
css.toColor('#75507b'),
css.toColor('#06989a'),
css.toColor('#d3d7cf'),
// bright:
fromCss('#555753'),
fromCss('#ef2929'),
fromCss('#8ae234'),
fromCss('#fce94f'),
fromCss('#729fcf'),
fromCss('#ad7fa8'),
fromCss('#34e2e2'),
fromCss('#eeeeec')
css.toColor('#555753'),
css.toColor('#ef2929'),
css.toColor('#8ae234'),
css.toColor('#fce94f'),
css.toColor('#729fcf'),
css.toColor('#ad7fa8'),
css.toColor('#34e2e2'),
css.toColor('#eeeeec')
]
} as any);
lineData = createEmptyLineData(2);
@@ -8,7 +8,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { IOptionsService } from 'common/services/Services';
import { ensureContrastRatio, rgbaToColor } from 'browser/Color';
import { color, rgba } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
export const BOLD_CLASS = 'xterm-bold';
@@ -101,7 +101,11 @@ export class DomRendererRowFactory {
charElement.classList.add(UNDERLINE_CLASS);
}
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
if (this._workCell.isInvisible()) {
charElement.textContent = WHITESPACE_CELL_CHAR;
} else {
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
}
let fg = this._workCell.getFgColor();
let fgColorMode = this._workCell.getFgColorMode();
@@ -129,7 +133,7 @@ export class DomRendererRowFactory {
}
break;
case Attributes.CM_RGB:
const color = rgbaToColor(
const color = rgba.toColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
@@ -178,7 +182,7 @@ export class DomRendererRowFactory {
// Calculate and store in cache
if (adjustedColor === undefined) {
adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
adjustedColor = color.ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
}
-171
View File
@@ -1,171 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { fill } from 'common/TypedArrayUtils';
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const COMBINING_BMP = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// binary search
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
return 1;
}
const control = opts.control | 0;
// create lookup table for BMP plane
const table = new Uint8Array(65536);
fill(table, 1);
table[0] = opts.nul;
// control chars
fill(table, opts.control, 1, 32);
fill(table, opts.control, 0x7f, 0xa0);
// apply wide char rules first
// wide chars
fill(table, 2, 0x1100, 0x1160);
table[0x2329] = 2;
table[0x232a] = 2;
fill(table, 2, 0x2e80, 0xa4d0);
table[0x303f] = 1; // wrongly in last line
fill(table, 2, 0xac00, 0xd7a4);
fill(table, 2, 0xf900, 0xfb00);
fill(table, 2, 0xfe10, 0xfe1a);
fill(table, 2, 0xfe30, 0xfe70);
fill(table, 2, 0xff00, 0xff61);
fill(table, 2, 0xffe0, 0xffe7);
// apply combining last to ensure we overwrite
// wrongly wide set chars:
// the original algo evals combining first and falls
// through to wide check so we simply do here the opposite
// combining 0
for (let r = 0; r < COMBINING_BMP.length; ++r) {
fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1);
}
return function (num: number): number {
if (num < 32) {
return control | 0;
}
if (num < 127) {
return 1;
}
if (num < 65536) {
return table[num];
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
/**
* Get the terminal cell width for a string.
*/
export function getStringCellWidth(s: string): number {
let result = 0;
const length = s.length;
for (let i = 0; i < length; ++i) {
let code = s.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
if (++i >= length) {
// this should not happen with strings retrieved from
// Buffer.translateToString as it converts from UTF-32
// and therefore always should contain the second part
// for any other string we still have to handle it somehow:
// simply treat the lonely surrogate first as a single char (UCS-2 behavior)
return result + wcwidth(code);
}
const second = s.charCodeAt(i);
// convert surrogate pair to high codepoint only for valid second part (UTF-16)
// otherwise treat them independently (UCS-2 behavior)
if (0xDC00 <= second && second <= 0xDFFF) {
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
result += wcwidth(second);
}
}
result += wcwidth(code);
}
return result;
}
+15 -15
View File
@@ -5,28 +5,28 @@
import { IDisposable } from 'common/Types';
interface IListener<T> {
(e: T): void;
interface IListener<T, U = void> {
(arg1: T, arg2: U): void;
}
export interface IEvent<T> {
(listener: (e: T) => any): IDisposable;
export interface IEvent<T, U = void> {
(listener: (arg1: T, arg2: U) => any): IDisposable;
}
export interface IEventEmitter<T> {
event: IEvent<T>;
fire(data: T): void;
export interface IEventEmitter<T, U = void> {
event: IEvent<T, U>;
fire(arg1: T, arg2: U): void;
dispose(): void;
}
export class EventEmitter<T> implements IEventEmitter<T> {
private _listeners: IListener<T>[] = [];
private _event?: IEvent<T>;
export class EventEmitter<T, U = void> implements IEventEmitter<T, U> {
private _listeners: IListener<T, U>[] = [];
private _event?: IEvent<T, U>;
private _disposed: boolean = false;
public get event(): IEvent<T> {
public get event(): IEvent<T, U> {
if (!this._event) {
this._event = (listener: (e: T) => any) => {
this._event = (listener: (arg1: T, arg2: U) => any) => {
this._listeners.push(listener);
const disposable = {
dispose: () => {
@@ -46,13 +46,13 @@ export class EventEmitter<T> implements IEventEmitter<T> {
return this._event;
}
public fire(data: T): void {
const queue: IListener<T>[] = [];
public fire(arg1: T, arg2: U): void {
const queue: IListener<T, U>[] = [];
for (let i = 0; i < this._listeners.length; i++) {
queue.push(this._listeners[i]);
}
for (let i = 0; i < queue.length; i++) {
queue[i].call(undefined, data);
queue[i].call(undefined, arg1, arg2);
}
}
+35 -4
View File
@@ -3,13 +3,14 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
import { IBufferSet, IBuffer } from 'common/buffer/Types';
import { BufferSet } from 'common/buffer/BufferSet';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset } from 'common/Types';
import { UnicodeV6 } from 'common/input/UnicodeV6';
export class MockBufferService implements IBufferService {
serviceBrand: any;
@@ -42,12 +43,27 @@ export class MockCoreMouseService implements ICoreMouseService {
}
}
export class MockCharsetService implements ICharsetService {
serviceBrand: any;
charset: ICharset | undefined;
glevel: number = 0;
charsets: readonly ICharset[] = [];
reset(): void {}
setgLevel(g: number): void {}
setgCharset(g: number, charset: ICharset): void {}
}
export class MockCoreService implements ICoreService {
serviceBrand: any;
isCursorInitialized: boolean = false;
isCursorHidden: boolean = false;
isFocused: boolean = false;
serviceBrand: any;
decPrivateModes: IDecPrivateModes = {} as any;
decPrivateModes: IDecPrivateModes = {
applicationCursorKeys: false,
applicationKeypad: false,
origin: false,
wraparound: true
};
onData: IEvent<string> = new EventEmitter<string>().event;
onUserInput: IEvent<void> = new EventEmitter<void>().event;
onBinary: IEvent<string> = new EventEmitter<string>().event;
@@ -90,3 +106,18 @@ export class MockOptionsService implements IOptionsService {
throw new Error('Method not implemented.');
}
}
// defaults to V6 always to keep tests passing
export class MockUnicodeService implements IUnicodeService {
private _provider = new UnicodeV6();
register(provider: IUnicodeVersionProvider): void {
throw new Error('Method not implemented.');
}
versions: string[] = [];
activeVersion: string = '';
onChange: IEvent<string> = new EventEmitter<string>().event;
wcwidth = (codepoint: number) => this._provider.wcwidth(codepoint);
getStringCellWidth(s: string): number {
throw new Error('Method not implemented.');
}
}
+36 -4
View File
@@ -62,7 +62,7 @@ export interface IKeyboardResult {
}
export interface ICharset {
[key: string]: string;
[key: string]: string | undefined;
}
export type CharData = [number, string, number, number];
@@ -93,6 +93,7 @@ export interface IAttributeData {
isBgPalette(): boolean;
isFgDefault(): boolean;
isBgDefault(): boolean;
isAttributeDefault(): boolean;
// colors
getFgColor(): number;
@@ -123,9 +124,9 @@ export interface IBufferLine {
setCell(index: number, cell: ICellData): void;
setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
addCodepointToCell(index: number, codePoint: number): void;
insertCells(pos: number, n: number, ch: ICellData): void;
deleteCells(pos: number, n: number, fill: ICellData): void;
replaceCells(start: number, end: number, fill: ICellData): void;
insertCells(pos: number, n: number, ch: ICellData, eraseAttr?: IAttributeData): void;
deleteCells(pos: number, n: number, fill: ICellData, eraseAttr?: IAttributeData): void;
replaceCells(start: number, end: number, fill: ICellData, eraseAttr?: IAttributeData): void;
resize(cols: number, fill: ICellData): void;
fill(fillCellData: ICellData): void;
copyFrom(line: IBufferLine): void;
@@ -152,6 +153,9 @@ export interface IMarker extends IDisposable {
export interface IDecPrivateModes {
applicationCursorKeys: boolean;
applicationKeypad: boolean;
origin: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
export interface IRowRange {
@@ -254,3 +258,31 @@ export interface ICoreMouseProtocol {
* (DEFAULT only supports up to 223 1-based as coord value).
*/
export type CoreMouseEncoding = (event: ICoreMouseEvent) => string;
/**
* windowOptions
*/
export interface IWindowOptions {
restoreWin?: boolean;
minimizeWin?: boolean;
setWinPosition?: boolean;
setWinSizePixels?: boolean;
raiseWin?: boolean;
lowerWin?: boolean;
refreshWin?: boolean;
setWinSizeChars?: boolean;
maximizeWin?: boolean;
fullscreenWin?: boolean;
getWinState?: boolean;
getWinPosition?: boolean;
getWinSizePixels?: boolean;
getScreenSizePixels?: boolean;
getCellSizePixels?: boolean;
getWinSizeChars?: boolean;
getScreenSizeChars?: boolean;
getIconTitle?: boolean;
getWinTitle?: boolean;
pushTitle?: boolean;
popTitle?: boolean;
setWinLines?: boolean;
}
+1 -1
View File
@@ -6,7 +6,7 @@
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService } from 'common/services/Services';
export function handleWindowsModeLineFeed(bufferService: IBufferService): void {
export function updateWindowsModeWrappedState(bufferService: IBufferService): void {
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
+1
View File
@@ -47,6 +47,7 @@ export class AttributeData implements IAttributeData {
public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }
public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }
public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }
public isAttributeDefault(): boolean { return this.fg === 0 && this.bg === 0; }
// colors
public getFgColor(): number {
+90
View File
@@ -366,4 +366,94 @@ describe('BufferLine', function(): void {
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
});
describe('correct fullwidth handling', () => {
function populate(line: BufferLine): void {
const cell = CellData.fromCharData([1, '¥', 2, '¥'.charCodeAt(0)]);
for (let i = 0; i < line.length; i += 2) {
line.setCell(i, cell);
}
}
it('insert - wide char at pos', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.insertCells(9, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), '¥¥¥¥ a');
line.insertCells(8, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), '¥¥¥¥a ');
line.insertCells(1, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' a ¥¥¥a');
});
it('insert - wide char at end', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.insertCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaa¥¥¥ ');
line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaa a ¥¥');
line.insertCells(4, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaa aa ¥ ');
});
it('delete', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.deleteCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' ¥¥¥¥a');
line.deleteCells(5, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' ¥¥¥aaa');
line.deleteCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' ¥¥aaaaa');
});
it('replace - start at 0', () => {
let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 1, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'a ¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aa¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaa ¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaaaaaaa¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaaaaaaaa ');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(0, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), 'aaaaaaaaaa');
});
it('replace - start at 1', () => {
let line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' a¥¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 3, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' aa ¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' aaa¥¥¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 8, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' aaaaaaa¥');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 9, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' aaaaaaaa ');
line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
populate(line);
line.replaceCells(1, 10, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.assert.equal(line.translateToString(), ' aaaaaaaaa');
});
});
});
+34 -4
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { CharData, IBufferLine, ICellData } from 'common/Types';
import { CharData, IBufferLine, ICellData, IAttributeData } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
@@ -228,8 +228,14 @@ export class BufferLine implements IBufferLine {
}
}
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
public insertCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void {
pos %= this.length;
// handle fullwidth at pos: reset cell one to the left if pos is second cell of a wide char
if (pos && this.getWidth(pos - 1) === 2) {
this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
if (n < this.length - pos) {
const cell = new CellData();
for (let i = this.length - pos - n - 1; i >= 0; --i) {
@@ -243,9 +249,14 @@ export class BufferLine implements IBufferLine {
this.setCell(i, fillCellData);
}
}
// handle fullwidth at line end: reset last cell if it is first cell of a wide char
if (this.getWidth(this.length - 1) === 2) {
this.setCellFromCodePoint(this.length - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
}
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
public deleteCells(pos: number, n: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void {
pos %= this.length;
if (n < this.length - pos) {
const cell = new CellData();
@@ -260,9 +271,28 @@ export class BufferLine implements IBufferLine {
this.setCell(i, fillCellData);
}
}
// handle fullwidth at pos:
// - reset pos-1 if wide char
// - reset pos if width==0 (previous second cell of a wide char)
if (pos && this.getWidth(pos - 1) === 2) {
this.setCellFromCodePoint(pos - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
if (this.getWidth(pos) === 0 && !this.hasContent(pos)) {
this.setCellFromCodePoint(pos, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
}
public replaceCells(start: number, end: number, fillCellData: ICellData): void {
public replaceCells(start: number, end: number, fillCellData: ICellData, eraseAttr?: IAttributeData): void {
// handle fullwidth at start: reset cell one to the left if start is second cell of a wide char
if (start && this.getWidth(start - 1) === 2) {
this.setCellFromCodePoint(start - 1, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
// handle fullwidth at last cell + 1: reset to empty cell if it is second part of a wide char
if (end < this.length && this.getWidth(end - 1) === 2) {
this.setCellFromCodePoint(end, 0, 1, eraseAttr?.fg || 0, eraseAttr?.bg || 0);
}
while (start < end && start < this.length) {
this.setCell(start++, fillCellData);
}
+6 -6
View File
@@ -30,14 +30,14 @@ export const DEFAULT_CHARSET: ICharset | null = CHARSETS['B'];
CHARSETS['0'] = {
'`': '\u25c6', // '◆'
'a': '\u2592', // '▒'
'b': '\u0009', // '\t'
'c': '\u000c', // '\f'
'd': '\u000d', // '\r'
'e': '\u000a', // '\n'
'b': '\u2409', // '␉' (HT)
'c': '\u240c', // '␌' (FF)
'd': '\u240d', // '␍' (CR)
'e': '\u240a', // '␊' (LF)
'f': '\u00b0', // '°'
'g': '\u00b1', // '±'
'h': '\u2424', // '\u2424' (NL)
'i': '\u000b', // '\v'
'h': '\u2424', // '␤' (NL)
'i': '\u240b', // '␋' (VT)
'j': '\u2518', // '┘'
'k': '\u2510', // '┐'
'l': '\u250c', // '┌'
@@ -1,12 +1,15 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { wcwidth } from 'common/CharWidth';
import { UnicodeV6 } from 'common/input/UnicodeV6';
it('wcwidth should match all values from the old implementation', function(): void {
// new implementation
const wcwidthNew = (new UnicodeV6()).wcwidth;
// old implementation
const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
@@ -177,6 +180,6 @@ it('wcwidth should match all values from the old implementation', function(): vo
// test full BMP range old vs new implmenetation
for (let i = 0; i < 65536; ++i) {
assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`);
assert.equal(wcwidthNew(i), wcwidthOld(i), `mismatch for i: ${i}`);
}
});
+133
View File
@@ -0,0 +1,133 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IUnicodeVersionProvider } from 'common/services/Services';
import { fill } from 'common/TypedArrayUtils';
type CharWidth = 0 | 1 | 2;
const BMP_COMBINING = [
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const HIGH_COMBINING = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
[0xE0100, 0xE01EF]
];
// BMP lookup table, lazy initialized during first addon loading
let table: Uint8Array;
function bisearch(ucs: number, data: number[][]): boolean {
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1]) {
min = mid + 1;
} else if (ucs < data[mid][0]) {
max = mid - 1;
} else {
return true;
}
}
return false;
}
export class UnicodeV6 implements IUnicodeVersionProvider {
public readonly version = '6';
constructor() {
// init lookup table once
if (!table) {
table = new Uint8Array(65536);
fill(table, 1);
table[0] = 0;
// control chars
fill(table, 0, 1, 32);
fill(table, 0, 0x7f, 0xa0);
// apply wide char rules first
// wide chars
fill(table, 2, 0x1100, 0x1160);
table[0x2329] = 2;
table[0x232a] = 2;
fill(table, 2, 0x2e80, 0xa4d0);
table[0x303f] = 1; // wrongly in last line
fill(table, 2, 0xac00, 0xd7a4);
fill(table, 2, 0xf900, 0xfb00);
fill(table, 2, 0xfe10, 0xfe1a);
fill(table, 2, 0xfe30, 0xfe70);
fill(table, 2, 0xff00, 0xff61);
fill(table, 2, 0xffe0, 0xffe7);
// apply combining last to ensure we overwrite
// wrongly wide set chars:
// the original algo evals combining first and falls
// through to wide check so we simply do here the opposite
// combining 0
for (let r = 0; r < BMP_COMBINING.length; ++r) {
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
}
}
}
public wcwidth(num: number): CharWidth {
if (num < 32) return 0;
if (num < 127) return 1;
if (num < 65536) return table[num] as CharWidth;
if (bisearch(num, HIGH_COMBINING)) return 0;
if ((num >= 0x20000 && num <= 0x2fffd) || (num >= 0x30000 && num <= 0x3fffd)) return 2;
return 1;
}
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharsetService } from 'common/services/Services';
import { ICharset } from 'common/Types';
export class CharsetService implements ICharsetService {
serviceBrand: any;
public charset: ICharset | undefined;
public charsets: ICharset[] = [];
public glevel: number = 0;
public reset(): void {
this.charset = undefined;
this.charsets = [];
this.glevel = 0;
}
public setgLevel(g: number): void {
this.glevel = g;
this.charset = this.charsets[g];
}
public setgCharset(g: number, charset: ICharset): void {
this.charsets[g] = charset;
if (this.glevel === g) {
this.charset = charset;
}
}
}
+5 -2
View File
@@ -5,11 +5,14 @@
import { ICoreService, ILogService, IOptionsService, IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { IDecPrivateModes } from 'common/Types';
import { IDecPrivateModes, ICharset } from 'common/Types';
import { clone } from 'common/Clone';
const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
applicationCursorKeys: false
applicationCursorKeys: false,
applicationKeypad: false,
origin: false,
wraparound: true // defaults: xterm - true, vt100 - false
});
export class CoreService implements ICoreService {
+5 -1
View File
@@ -36,7 +36,7 @@ export class ServiceCollection {
return this._entries.has(id);
}
get<T>(id: IServiceIdentifier<T>): T {
get<T>(id: IServiceIdentifier<T>): T | undefined {
return this._entries.get(id);
}
}
@@ -52,6 +52,10 @@ export class InstantiationService implements IInstantiationService {
this._services.set(id, instance);
}
public getService<T>(id: IServiceIdentifier<T>): T | undefined {
return this._services.get(id);
}
public createInstance<T>(ctor: any, ...args: any[]): any {
const serviceDependencies = getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
+8 -4
View File
@@ -20,6 +20,7 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
rows: 24,
cursorBlink: false,
cursorStyle: 'block',
cursorWidth: 1,
bellSound: DEFAULT_BELL_SOUND,
bellStyle: 'none',
drawBoldTextInBrightColors: true,
@@ -44,14 +45,13 @@ export const DEFAULT_OPTIONS: ITerminalOptions = Object.freeze({
theme: {},
rightClickSelectsWord: isMac,
rendererType: 'canvas',
windowOptions: {},
windowsMode: false,
wordSeparator: ' ()[]{}\',"`',
convertEol: false,
termName: 'xterm',
screenKeys: false,
cancelEvents: false,
useFlowControl: false,
wordSeparator: ' ()[]{}\',:;"`'
cancelEvents: false
});
/**
@@ -111,6 +111,9 @@ export class OptionsService implements IOptionsService {
value = DEFAULT_OPTIONS[key];
}
break;
case 'cursorWidth':
value = Math.floor(value);
// Fall through for bounds check
case 'lineHeight':
case 'tabStopWidth':
if (value < 1) {
@@ -119,6 +122,7 @@ export class OptionsService implements IOptionsService {
break;
case 'minimumContrastRatio':
value = Math.max(1, Math.min(21, Math.round(value * 10) / 10));
break;
case 'scrollback':
value = Math.min(value, 4294967295);
if (value < 0) {
+54 -3
View File
@@ -5,7 +5,7 @@
import { IEvent } from 'common/EventEmitter';
import { IBuffer, IBufferSet } from 'common/buffer/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType } from 'common/Types';
import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions } from 'common/Types';
import { createDecorator } from 'common/services/ServiceRegistry';
export const IBufferService = createDecorator<IBufferService>('BufferService');
@@ -64,6 +64,7 @@ export interface ICoreService {
*/
isCursorInitialized: boolean;
isCursorHidden: boolean;
readonly decPrivateModes: IDecPrivateModes;
readonly onData: IEvent<string>;
@@ -89,6 +90,30 @@ export interface ICoreService {
triggerBinaryEvent(data: string): void;
}
export const ICharsetService = createDecorator<ICharsetService>('CharsetService');
export interface ICharsetService {
serviceBrand: any;
charset: ICharset | undefined;
readonly glevel: number;
readonly charsets: ReadonlyArray<ICharset>;
reset(): void;
/**
* Set the G level of the terminal.
* @param g
*/
setgLevel(g: number): void;
/**
* Set the charset for the given G level of the terminal.
* @param g
* @param charset
*/
setgCharset(g: number, charset: ICharset): void;
}
export const IDirtyRowService = createDecorator<IDirtyRowService>('DirtyRowService');
export interface IDirtyRowService {
serviceBrand: any;
@@ -146,6 +171,7 @@ export interface IConstructorSignature8<A1, A2, A3, A4, A5, A6, A7, A8, T> {
export const IInstantiationService = createDecorator<IInstantiationService>('InstantiationService');
export interface IInstantiationService {
setService<T>(id: IServiceIdentifier<T>, instance: T): void;
getService<T>(id: IServiceIdentifier<T>): T | undefined;
createInstance<T>(ctor: IConstructorSignature0<T>): T;
createInstance<A1, T>(ctor: IConstructorSignature1<A1, T>, first: A1): T;
@@ -191,6 +217,7 @@ export interface IPartialTerminalOptions {
cols?: number;
cursorBlink?: boolean;
cursorStyle?: 'block' | 'underline' | 'bar';
cursorWidth?: number;
disableStdin?: boolean;
drawBoldTextInBrightColors?: boolean;
fastScrollModifier?: 'alt' | 'ctrl' | 'shift';
@@ -214,6 +241,7 @@ export interface IPartialTerminalOptions {
theme?: ITheme;
windowsMode?: boolean;
wordSeparator?: string;
windowOptions?: IWindowOptions;
}
export interface ITerminalOptions {
@@ -223,6 +251,7 @@ export interface ITerminalOptions {
cols: number;
cursorBlink: boolean;
cursorStyle: 'block' | 'underline' | 'bar';
cursorWidth: number;
disableStdin: boolean;
drawBoldTextInBrightColors: boolean;
fastScrollModifier: 'alt' | 'ctrl' | 'shift' | undefined;
@@ -246,14 +275,13 @@ export interface ITerminalOptions {
tabStopWidth: number;
theme: ITheme;
windowsMode: boolean;
windowOptions: IWindowOptions;
wordSeparator: string;
[key: string]: any;
cancelEvents: boolean;
convertEol: boolean;
screenKeys: boolean;
termName: string;
useFlowControl: boolean;
}
export interface ITheme {
@@ -279,3 +307,26 @@ export interface ITheme {
brightCyan?: string;
brightWhite?: string;
}
export const IUnicodeService = createDecorator<IUnicodeService>('UnicodeService');
export interface IUnicodeService {
/** Register an Unicode version provider. */
register(provider: IUnicodeVersionProvider): void;
/** Registered Unicode versions. */
readonly versions: string[];
/** Currently active version. */
activeVersion: string;
/** Event triggered, when activate version changed. */
readonly onChange: IEvent<string>;
/**
* Unicode version dependent
*/
wcwidth(codepoint: number): number;
getStringCellWidth(s: string): number;
}
export interface IUnicodeVersionProvider {
readonly version: string;
wcwidth(ucs: number): 0 | 1 | 2;
}
@@ -0,0 +1,50 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { UnicodeService } from 'common/services/UnicodeService';
import { IUnicodeVersionProvider } from 'common/services/Services';
class DummyProvider implements IUnicodeVersionProvider {
version = '123';
wcwidth(n: number): 0 | 1 | 2 {
return 2;
}
}
describe('unicode provider', () => {
let us: UnicodeService;
beforeEach(() => {
us = new UnicodeService();
});
it('default to V6', () => {
assert.equal(us.activeVersion, '6');
assert.deepEqual(us.versions, ['6']);
assert.doesNotThrow(() => { us.activeVersion = '6'; }, `unknown Unicode version "6"`);
assert.equal(us.getStringCellWidth('hello'), 5);
});
it('activate should throw for unknown version', () => {
assert.throws(() => { us.activeVersion = '55'; }, 'unknown Unicode version "55"');
});
it('should notify about version change', () => {
const notes: string[] = [];
us.onChange(version => notes.push(version));
const dummyProvider = new DummyProvider();
us.register(dummyProvider);
us.activeVersion = dummyProvider.version;
assert.deepEqual(notes, [dummyProvider.version]);
});
it('correctly changes provider impl', () => {
assert.equal(us.getStringCellWidth('hello'), 5);
const dummyProvider = new DummyProvider();
us.register(dummyProvider);
us.activeVersion = dummyProvider.version;
assert.equal(us.getStringCellWidth('hello'), 2 * 5);
});
it('wcwidth V6 emoji test', () => {
const widthV6 = us.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣');
assert.equal(widthV6, 10);
});
});
+80
View File
@@ -0,0 +1,80 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { UnicodeV6 } from 'common/input/UnicodeV6';
export class UnicodeService implements IUnicodeService {
private _providers: {[key: string]: IUnicodeVersionProvider} = Object.create(null);
private _active: string = '';
private _activeProvider: IUnicodeVersionProvider;
private _onChange = new EventEmitter<string>();
public get onChange(): IEvent<string> { return this._onChange.event; }
constructor() {
const defaultProvider = new UnicodeV6();
this.register(defaultProvider);
this._active = defaultProvider.version;
this._activeProvider = defaultProvider;
}
public get versions(): string[] {
return Object.keys(this._providers);
}
public get activeVersion(): string {
return this._active;
}
public set activeVersion(version: string) {
if (!this._providers[version]) {
throw new Error(`unknown Unicode version "${version}"`);
}
this._active = version;
this._activeProvider = this._providers[version];
this._onChange.fire(version);
}
public register(provider: IUnicodeVersionProvider): void {
this._providers[provider.version] = provider;
}
/**
* Unicode version dependent interface.
*/
public wcwidth(num: number): number {
return this._activeProvider.wcwidth(num);
}
public getStringCellWidth(s: string): number {
let result = 0;
const length = s.length;
for (let i = 0; i < length; ++i) {
let code = s.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
if (++i >= length) {
// this should not happen with strings retrieved from
// Buffer.translateToString as it converts from UTF-32
// and therefore always should contain the second part
// for any other string we still have to handle it somehow:
// simply treat the lonely surrogate first as a single char (UCS-2 behavior)
return result + this.wcwidth(code);
}
const second = s.charCodeAt(i);
// convert surrogate pair to high codepoint only for valid second part (UTF-16)
// otherwise treat them independently (UCS-2 behavior)
if (0xDC00 <= second && second <= 0xDFFF) {
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
result += this.wcwidth(second);
}
}
result += this.wcwidth(code);
}
return result;
}
}
+59 -23
View File
@@ -3,10 +3,11 @@
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier } from 'xterm';
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, IUnicodeHandling, IUnicodeVersionProvider } from 'xterm';
import { ITerminal } from '../Types';
import { IBufferLine } from 'common/Types';
import { IBufferLine, ICellData } from 'common/Types';
import { IBuffer } from 'common/buffer/Types';
import { CellData } from 'common/buffer/CellData';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../browser/LocalizableStrings';
import { IEvent } from 'common/EventEmitter';
@@ -41,6 +42,9 @@ export class Terminal implements ITerminalApi {
}
return this._parser;
}
public get unicode(): IUnicodeHandling {
return new UnicodeApi(this._core);
}
public get textarea(): HTMLTextAreaElement | undefined { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
@@ -74,10 +78,13 @@ export class Terminal implements ITerminalApi {
public deregisterCharacterJoiner(joinerId: number): void {
this._core.deregisterCharacterJoiner(joinerId);
}
public addMarker(cursorYOffset: number): IMarker {
public registerMarker(cursorYOffset: number): IMarker {
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
}
public addMarker(cursorYOffset: number): IMarker {
return this.registerMarker(cursorYOffset);
}
public hasSelection(): boolean {
return this._core.hasSelection();
}
@@ -140,10 +147,8 @@ export class Terminal implements ITerminalApi {
this._core.paste(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
public getOption(key: 'colors'): string[];
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean;
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
public getOption(key: 'handler'): (data: string) => void;
public getOption(key: string): any;
public getOption(key: any): any {
return this._core.optionsService.getOption(key);
@@ -153,10 +158,8 @@ export class Terminal implements ITerminalApi {
public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void;
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
public setOption(key: 'colors', value: string[]): void;
public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void;
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
public setOption(key: 'handler', value: (data: string) => void): void;
public setOption(key: 'theme', value: ITheme): void;
public setOption(key: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void;
@@ -187,7 +190,7 @@ export class Terminal implements ITerminalApi {
}
class BufferApiView implements IBufferApi {
constructor(private _buffer: IBuffer) {}
constructor(private _buffer: IBuffer) { }
public get cursorY(): number { return this._buffer.y; }
public get cursorX(): number { return this._buffer.x; }
@@ -201,42 +204,75 @@ class BufferApiView implements IBufferApi {
}
return new BufferLineApiView(line);
}
public getNullCell(): IBufferCellApi { return new CellData(); }
}
class BufferLineApiView implements IBufferLineApi {
constructor(private _line: IBufferLine) {}
constructor(private _line: IBufferLine) { }
public get isWrapped(): boolean { return this._line.isWrapped; }
public getCell(x: number): IBufferCellApi | undefined {
public get length(): number { return this._line.length; }
public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {
if (x < 0 || x >= this._line.length) {
return undefined;
}
return new BufferCellApiView(this._line, x);
if (cell) {
this._line.loadCell(x, <ICellData>cell);
return cell;
}
return this._line.loadCell(x, new CellData());
}
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
return this._line.translateToString(trimRight, startColumn, endColumn);
}
}
class BufferCellApiView implements IBufferCellApi {
constructor(private _line: IBufferLine, private _x: number) {}
public get char(): string { return this._line.getString(this._x); }
public get width(): number { return this._line.getWidth(this._x); }
}
class ParserApi implements IParser {
constructor(private _core: ITerminal) {}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this.registerCsiHandler(id, callback);
}
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this.registerDcsHandler(id, callback);
}
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this._core.addEscHandler(id, handler);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this.registerEscHandler(id, handler);
}
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this.registerOscHandler(ident, callback);
}
}
class UnicodeApi implements IUnicodeHandling {
constructor(private _core: ITerminal) {}
public register(provider: IUnicodeVersionProvider): void {
this._core.unicodeService.register(provider);
}
public get versions(): string[] {
return this._core.unicodeService.versions;
}
public get activeVersion(): string {
return this._core.unicodeService.activeVersion;
}
public set activeVersion(version: string) {
this._core.unicodeService.activeVersion = version;
}
}
+2 -2
View File
@@ -98,8 +98,8 @@ async function sumWidths(start: number, end: number, sentinel: string): Promise<
if (!cell) {
break;
}
window.result += cell.width;
if (cell.char === '${sentinel}') {
window.result += cell.getWidth();
if (cell.getChars() === '${sentinel}') {
return;
}
}
+48
View File
@@ -345,6 +345,44 @@ describe('InputHandler Integration Tests', function(): void {
`);
await pollFor(page, () => getLinesAsArray(3), ['#', ' #', 'abcd####']);
});
describe('Window Options - CSI Ps ; Ps ; Ps t', () => {
it('should be disabled by default', async function() {
await page.evaluate(`(() => {
window._stack = [];
const _h = window.term.onData(data => window._stack.push(data));
window.term.write('\x1b[14t');
window.term.write('\x1b[16t');
window.term.write('\x1b[18t');
window.term.write('\x1b[20t');
window.term.write('\x1b[21t');
return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); }));
})()`);
await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), []);
});
it('14 - GetWinSizePixels', async function() {
await page.evaluate(`window.term.setOption('windowOptions', {getWinSizePixels: true});`);
await page.evaluate(`(() => {
window._stack = [];
const _h = window.term.onData(data => window._stack.push(data));
window.term.write('\x1b[14t');
return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); }));
})()`);
const d = await getDimensions();
await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[4;${d.height};${d.width}t`]);
});
it('16 - GetCellSizePixels', async function() {
await page.evaluate(`window.term.setOption('windowOptions', {getCellSizePixels: true});`);
await page.evaluate(`(() => {
window._stack = [];
const _h = window.term.onData(data => window._stack.push(data));
window.term.write('\x1b[16t');
return new Promise((r) => window.term.write('', () => { _h.dispose(); r(); }));
})()`);
const d = await getDimensions();
await pollFor(page, async () => await page.evaluate(`(() => _stack)()`), [`\x1b[6;${d.cellHeight};${d.cellWidth}t`]);
});
});
});
describe('ESC', () => {
@@ -403,3 +441,13 @@ async function getCursor(): Promise<{col: number, row: number}> {
})();
`);
}
async function getDimensions(): Promise<any> {
const dim = await page.evaluate(`term._core._renderService.dimensions`);
return {
cellWidth: dim.actualCellWidth.toFixed(0),
cellHeight: dim.actualCellHeight.toFixed(0),
width: dim.canvasWidth.toFixed(0),
height: dim.canvasHeight.toFixed(0)
};
}
+8 -8
View File
@@ -498,15 +498,15 @@ describe('API Integration Tests', function(): void {
await openTerminal({ cols: 5 });
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(-1)`), undefined);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(5)`), undefined);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getChars()`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getWidth()`), 1);
await writeSync(page, 'a文');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), 'a');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).char`), '文');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).width`), 2);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).char`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).width`), 0);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getChars()`), 'a');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).getWidth()`), 1);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).getChars()`), '文');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).getWidth()`), 2);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).getChars()`), '');
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).getWidth()`), 0);
});
});
});
+2 -2
View File
@@ -16,7 +16,7 @@
"paths": {
"common/*": [ "../../src/common/*" ],
"browser/*": [ "../../src/browser/*" ],
"Terminal": [ "../../src/Terminal" ]
"Terminal": ["../../src/Terminal"]
},
},
"include": [
@@ -31,4 +31,4 @@
{ "path": "../../src/common" },
{ "path": "../../src/browser" },
]
}
}
+4 -1
View File
@@ -8,7 +8,10 @@
{ "path": "./addons/xterm-addon-attach/src" },
{ "path": "./addons/xterm-addon-fit/src" },
{ "path": "./addons/xterm-addon-search/src" },
{ "path": "./addons/xterm-addon-unicode11/src" },
{ "path": "./addons/xterm-addon-web-links/src" },
{ "path": "./addons/xterm-addon-webgl/src" }
{ "path": "./addons/xterm-addon-webgl/src" },
{ "path": "./addons/xterm-addon-serialize/src" },
{ "path": "./addons/xterm-addon-serialize/benchmark" }
]
}
+334 -43
View File
@@ -72,6 +72,11 @@ declare module 'xterm' {
*/
cursorStyle?: 'block' | 'underline' | 'bar';
/**
* The width of the cursor in CSS pixels when `cursorStyle` is set to 'bar'.
*/
cursorWidth?: number;
/**
* Whether input should be disabled.
*/
@@ -227,6 +232,12 @@ declare module 'xterm' {
* double click to select work logic.
*/
wordSeparator?: string;
/**
* Enable various window manipulation and report features.
* All features are disabled by default for security reasons.
*/
windowOptions?: IWindowOptions;
}
/**
@@ -331,8 +342,8 @@ declare module 'xterm' {
* An event that can be listened to.
* @returns an `IDisposable` to stop listening.
*/
export interface IEvent<T> {
(listener: (e: T) => any): IDisposable;
export interface IEvent<T, U = void> {
(listener: (arg1: T, arg2: U) => any): IDisposable;
}
/**
@@ -374,6 +385,162 @@ declare module 'xterm' {
tooMuchOutput: string;
}
/**
* Enable various window manipulation and report features (CSI Ps ; Ps ; Ps t).
*
* Most settings have no default implementation, as they heavily rely on
* the embedding environment.
*
* To implement a feature, create a custom CSI hook like this:
* ```ts
* term.parser.addCsiHandler({final: 't'}, params => {
* const ps = params[0];
* switch (ps) {
* case XY:
* ... // your implementation for option XY
* return true; // signal Ps=XY was handled
* }
* return false; // any Ps that was not handled
* });
* ```
*
* Note on security:
* Most features are meant to deal with some information of the host machine
* where the terminal runs on. This is seen as a security risk possibly leaking
* sensitive data of the host to the program in the terminal. Therefore all options
* (even those without a default implementation) are guarded by the boolean flag
* and disabled by default.
*/
export interface IWindowOptions {
/**
* Ps=1 De-iconify window.
* No default implementation.
*/
restoreWin?: boolean;
/**
* Ps=2 Iconify window.
* No default implementation.
*/
minimizeWin?: boolean;
/**
* Ps=3 ; x ; y
* Move window to [x, y].
* No default implementation.
*/
setWinPosition?: boolean;
/**
* Ps = 4 ; height ; width
* Resize the window to given `height` and `width` in pixels.
* Omitted parameters should reuse the current height or width.
* Zero parameters should use the display's height or width.
* No default implementation.
*/
setWinSizePixels?: boolean;
/**
* Ps=5 Raise the window to the front of the stacking order.
* No default implementation.
*/
raiseWin?: boolean;
/**
* Ps=6 Lower the xterm window to the bottom of the stacking order.
* No default implementation.
*/
lowerWin?: boolean;
/** Ps=7 Refresh the window. */
refreshWin?: boolean;
/**
* Ps = 8 ; height ; width
* Resize the text area to given height and width in characters.
* Omitted parameters should reuse the current height or width.
* Zero parameters use the display's height or width.
* No default implementation.
*/
setWinSizeChars?: boolean;
/**
* Ps=9 ; 0 Restore maximized window.
* Ps=9 ; 1 Maximize window (i.e., resize to screen size).
* Ps=9 ; 2 Maximize window vertically.
* Ps=9 ; 3 Maximize window horizontally.
* No default implementation.
*/
maximizeWin?: boolean;
/**
* Ps=10 ; 0 Undo full-screen mode.
* Ps=10 ; 1 Change to full-screen.
* Ps=10 ; 2 Toggle full-screen.
* No default implementation.
*/
fullscreenWin?: boolean;
/** Ps=11 Report xterm window state.
* If the xterm window is non-iconified, it returns "CSI 1 t".
* If the xterm window is iconified, it returns "CSI 2 t".
* No default implementation.
*/
getWinState?: boolean;
/**
* Ps=13 Report xterm window position. Result is "CSI 3 ; x ; y t".
* Ps=13 ; 2 Report xterm text-area position. Result is "CSI 3 ; x ; y t".
* No default implementation.
*/
getWinPosition?: boolean;
/**
* Ps=14 Report xterm text area size in pixels. Result is "CSI 4 ; height ; width t".
* Ps=14 ; 2 Report xterm window size in pixels. Result is "CSI 4 ; height ; width t".
* Has a default implementation.
*/
getWinSizePixels?: boolean;
/**
* Ps=15 Report size of the screen in pixels. Result is "CSI 5 ; height ; width t".
* No default implementation.
*/
getScreenSizePixels?: boolean;
/**
* Ps=16 Report xterm character cell size in pixels. Result is "CSI 6 ; height ; width t".
* Has a default implementation.
*/
getCellSizePixels?: boolean;
/**
* Ps=18 Report the size of the text area in characters. Result is "CSI 8 ; height ; width t".
* Has a default implementation.
*/
getWinSizeChars?: boolean;
/**
* Ps=19 Report the size of the screen in characters. Result is "CSI 9 ; height ; width t".
* No default implementation.
*/
getScreenSizeChars?: boolean;
/**
* Ps=20 Report xterm window's icon label. Result is "OSC L label ST".
* No default implementation.
*/
getIconTitle?: boolean;
/**
* Ps=21 Report xterm window's title. Result is "OSC l label ST".
* No default implementation.
*/
getWinTitle?: boolean;
/**
* Ps=22 ; 0 Save xterm icon and window title on stack.
* Ps=22 ; 1 Save xterm icon title on stack.
* Ps=22 ; 2 Save xterm window title on stack.
* All variants have a default implementation.
*/
pushTitle?: boolean;
/**
* Ps=23 ; 0 Restore xterm icon and window title from stack.
* Ps=23 ; 1 Restore xterm icon title from stack.
* Ps=23 ; 2 Restore xterm window title from stack.
* All variants have a default implementation.
*/
popTitle?: boolean;
/**
* Ps>=24 Resize to Ps lines (DECSLPP).
* DECSLPP is not implemented. This settings is also used to
* enable / disable DECCOLM (earlier variant of DECSLPP).
*/
setWinLines?: boolean;
}
/**
* The class that represents an xterm.js terminal.
*/
@@ -421,6 +588,12 @@ declare module 'xterm' {
*/
readonly parser: IParser;
/**
* (EXPERIMENTAL) Get the Unicode handling interface
* to register and switch Unicode version.
*/
readonly unicode: IUnicodeHandling;
/**
* Natural language strings that can be localized.
*/
@@ -439,7 +612,7 @@ declare module 'xterm' {
* Currently this is only used for a certain type of mouse reports that
* happen to be not UTF-8 compatible.
* The event value is a JS string, pass it to the underlying pty as
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
* binary data, e.g. `pty.write(Buffer.from(data, 'binary'))`.
* @returns an `IDisposable` to stop listening.
*/
onBinary: IEvent<string>;
@@ -607,6 +780,11 @@ declare module 'xterm' {
* alt buffer is active, undefined is returned.
* @param cursorYOffset The y position offset of the marker from the cursor.
*/
registerMarker(cursorYOffset: number): IMarker;
/**
* @deprecated use `registerMarker` instead.
*/
addMarker(cursorYOffset: number): IMarker;
/**
@@ -732,22 +910,12 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'colors'): string[];
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell' | 'windowsMode'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'handler'): (data: string) => void;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -789,25 +957,13 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'colors', value: string[]): void;
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'visualBell' | 'windowsMode', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'handler', value: (data: string) => void): void;
/**
* Sets an option on the terminal.
* @param key The option key.
@@ -961,6 +1117,13 @@ declare module 'xterm' {
* @param y The line index to get.
*/
getLine(y: number): IBufferLine | undefined;
/**
* Creates an empty cell object suitable as a cell reference in
* `line.getCell(x, cell)`. Use this to avoid costly recreation of
* cell objects when dealing with tons of cells.
*/
getNullCell(): IBufferCell;
}
/**
@@ -972,6 +1135,12 @@ declare module 'xterm' {
*/
readonly isWrapped: boolean;
/**
* The length of the line, all call to getCell beyond the length will result
* in `undefined`.
*/
readonly length: number;
/**
* Gets a cell from the line, or undefined if the line index does not exist.
*
@@ -980,8 +1149,11 @@ declare module 'xterm' {
* behavior.
*
* @param x The character index to get.
* @param cell Optional cell object to load data into for performance
* reasons. This is mainly useful when every cell in the buffer is being
* looped over to avoid creating new objects for every cell.
*/
getCell(x: number): IBufferCell | undefined;
getCell(x: number, cell?: IBufferCell): IBufferCell | undefined;
/**
* Gets the line as a string. Note that this is gets only the string for the
@@ -998,23 +1170,106 @@ declare module 'xterm' {
* Represents a single cell in the terminal's buffer.
*/
interface IBufferCell {
/**
* The character within the cell.
*/
readonly char: string;
/**
* The width of the character. Some examples:
*
* - This is `1` for most cells.
* - This is `2` for wide character like CJK glyphs.
* - This is `0` for cells immediately following cells with a width of `2`.
* - `1` for most cells.
* - `2` for wide character like CJK glyphs.
* - `0` for cells immediately following cells with a width of `2`.
*/
readonly width: number;
getWidth(): number;
/**
* The character(s) within the cell. Examples of what this can contain:
*
* - A normal width character
* - A wide character (eg. CJK)
* - An emoji
*/
getChars(): string;
/**
* Gets the UTF32 codepoint of single characters, if content is a combined
* string it returns the codepoint of the last character in the string.
*/
getCode(): number;
/**
* Gets the number representation of the foreground color mode, this can be
* used to perform quick comparisons of 2 cells to see if they're the same.
* Use `isFgRGB`, `isFgPalette` and `isFgDefault` to check what color mode
* a cell is.
*/
getFgColorMode(): number;
/**
* Gets the number representation of the background color mode, this can be
* used to perform quick comparisons of 2 cells to see if they're the same.
* Use `isBgRGB`, `isBgPalette` and `isBgDefault` to check what color mode
* a cell is.
*/
getBgColorMode(): number;
/**
* Gets a cell's foreground color number, this differs depending on what the
* color mode of the cell is:
*
* - Default: This should be 0, representing the default foreground color
* (CSI 39 m).
* - Palette: This is a number from 0 to 255 of ANSI colors (CSI 3(0-7) m,
* CSI 9(0-7) m, CSI 38 ; 5 ; 0-255 m).
* - RGB: A hex value representing a 'true color': 0xRRGGBB.
* (CSI 3 8 ; 2 ; Pi ; Pr ; Pg ; Pb)
*/
getFgColor(): number;
/**
* Gets a cell's background color number, this differs depending on what the
* color mode of the cell is:
*
* - Default: This should be 0, representing the default background color
* (CSI 49 m).
* - Palette: This is a number from 0 to 255 of ANSI colors
* (CSI 4(0-7) m, CSI 10(0-7) m, CSI 48 ; 5 ; 0-255 m).
* - RGB: A hex value representing a 'true color': 0xRRGGBB
* (CSI 4 8 ; 2 ; Pi ; Pr ; Pg ; Pb)
*/
getBgColor(): number;
/** Whether the cell has the bold attribute (CSI 1 m). */
isBold(): number;
/** Whether the cell has the inverse attribute (CSI 3 m). */
isItalic(): number;
/** Whether the cell has the inverse attribute (CSI 2 m). */
isDim(): number;
/** Whether the cell has the underline attribute (CSI 4 m). */
isUnderline(): number;
/** Whether the cell has the inverse attribute (CSI 5 m). */
isBlink(): number;
/** Whether the cell has the inverse attribute (CSI 7 m). */
isInverse(): number;
/** Whether the cell has the inverse attribute (CSI 8 m). */
isInvisible(): number;
/** Whether the cell is using the RGB foreground color mode. */
isFgRGB(): boolean;
/** Whether the cell is using the RGB background color mode. */
isBgRGB(): boolean;
/** Whether the cell is using the palette foreground color mode. */
isFgPalette(): boolean;
/** Whether the cell is using the palette background color mode. */
isBgPalette(): boolean;
/** Whether the cell is using the default foreground color mode. */
isFgDefault(): boolean;
/** Whether the cell is using the default background color mode. */
isBgDefault(): boolean;
/** Whether the cell has the default attribute (no color or style). */
isAttributeDefault(): boolean;
}
/**
* (EXPERIMENTAL) Data type to register a CSI, DCS or ESC callback in the parser
* Data type to register a CSI, DCS or ESC callback in the parser
* in the form:
* ESC I..I F
* CSI Prefix P..P I..I F
@@ -1058,7 +1313,7 @@ declare module 'xterm' {
}
/**
* (EXPERIMENTAL) Parser interface.
* Allows hooking into the parser for custom handling of escape sequences.
*/
export interface IParser {
/**
@@ -1073,7 +1328,7 @@ declare module 'xterm' {
* The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable;
/**
* Adds a handler for DCS escape sequences.
@@ -1092,7 +1347,7 @@ declare module 'xterm' {
* The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable;
/**
* Adds a handler for ESC escape sequences.
@@ -1105,7 +1360,7 @@ declare module 'xterm' {
* The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable;
registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable;
/**
* Adds a handler for OSC escape sequences.
@@ -1123,6 +1378,42 @@ declare module 'xterm' {
* The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
}
/**
* (EXPERIMENTAL) Unicode version provider.
* Used to register custom Unicode versions with `Terminal.unicode.register`.
*/
export interface IUnicodeVersionProvider {
/**
* String indicating the Unicode version provided.
*/
readonly version: string;
/**
* Unicode version dependent wcwidth implementation.
*/
wcwidth(codepoint: number): 0 | 1 | 2;
}
/**
* (EXPERIMENTAL) Unicode handling interface.
*/
export interface IUnicodeHandling {
/**
* Register a custom Unicode version provider.
*/
register(provider: IUnicodeVersionProvider): void;
/**
* Registered Unicode versions.
*/
readonly versions: ReadonlyArray<string>;
/**
* Getter/setter for active Unicode version.
*/
activeVersion: string;
}
}
+11 -11
View File
@@ -3421,17 +3421,17 @@ ms@2.1.1, ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
mustache@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/mustache/-/mustache-3.0.1.tgz#873855f23aa8a95b150fb96d9836edbc5a1d248a"
integrity sha512-jFI/4UVRsRYdUbuDTKT7KzfOp7FiD5WzYmmwNwXyUVypC0xjoTL78Fqc0jHUPIvvGD+6DQSPHIt1NE7D1ArsqA==
mute-stream@0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
nan@2.10.0:
version "2.10.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==
nan@^2.12.1:
nan@^2.12.1, nan@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c"
integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==
@@ -3530,12 +3530,12 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0"
tar "^4"
node-pty@0.7.6:
version "0.7.6"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.7.6.tgz#bff6148c9c5836ca7e73c7aaaec067dcbdac2f7b"
integrity sha512-ECzKUB7KkAFZ0cjyjMXp5WLJ+7YIZ1xnNmiiegOI6WdDaKABUNV5NbB1Dw9MXD4KrZipWII0wQ7RGZ6StU/7jA==
node-pty@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.9.0.tgz#8f9bcc0d1c5b970a3184ffd533d862c7eb6590a6"
integrity sha512-MBnCQl83FTYOu7B4xWw10AW77AAh7ThCE1VXEv+JeWj8mSpGo+0bwgsV+b23ljBFwEM9OmsOv3kM27iUPPm84g==
dependencies:
nan "2.10.0"
nan "^2.14.0"
nopt@^4.0.1:
version "4.0.1"