Compare commits

...
520 Commits
Author SHA1 Message Date
Paris Kasidiaris f5ba386c01 Bump version to 2.9.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-08-03 18:43:14 +00:00
Daniel Imms 0079677e43 Merge pull request #781 from mofux/cursor_color
Retain the color of a character for underline and bar cursor styles
2017-08-03 09:16:23 -07:00
mofux 106eda7067 Merge changes from #828 2017-08-02 17:42:43 +02:00
Paris Kasidiaris f13455965c Merge pull request #828 from sourcelair/issue-#827-fix-terminal-focus
Always focus terminal on click
2017-08-02 18:33:51 +03:00
thomas 5bc0ad879b Set xterm-color for cursor element
Prevent focus from getting lost when in vt mouse mode
Fix: cursor style not applied on init, set a class for block cursor
Make all cursors work under all conditions (focused, unfocused, blink)
Unify cursor rendering with normal rendering code
Make sure underline AND blink can be used in co-existence
Always show outlined block cursor in unfocused state
2017-08-02 15:41:18 +02:00
Paris Kasidiaris 72e85a352a Focus on mousedown, instead of click 2017-08-02 16:03:56 +03:00
Paris Kasidiaris 49c1ca3f57 Fix #827: Always focus terminal on click 2017-08-02 10:44:16 +03:00
Daniel Imms bcff69b79c Merge pull request #816 from joaomoreno/alt-selection
Alt key should force selection
2017-07-28 14:24:45 -07:00
Daniel Imms 3f7fec8c91 Merge branch 'master' into alt-selection 2017-07-28 14:07:43 -07:00
Daniel Imms 3479614c73 Disable selection override on Windows/Linux 2017-07-28 14:06:55 -07:00
Daniel Imms 34f075a8cc Merge pull request #822 from Tyriar/818_fix_save_restore_cursor
Fix save and restore cursor behavior
2017-07-28 08:58:03 -07:00
Daniel Imms ff165e8627 Merge branch 'master' into 818_fix_save_restore_cursor 2017-07-28 08:33:52 -07:00
Paris Kasidiaris cdcbf60e44 Merge pull request #823 from jpmasters/master
Issue #821 - Prevent TypeError in resize()
2017-07-28 15:02:38 +03:00
Jon Masters 2d4e617c95 Issue #821 - Prevent TypeError in resize() 2017-07-28 11:54:46 +00:00
Daniel Imms 6ea7758ab1 Add test for save and restore cursor 2017-07-27 16:35:48 -07:00
Daniel Imms 604959a319 Fix save and restore cursor behavior
This broke in #717.

Fixes #818
2017-07-27 16:28:27 -07:00
Joao Moreno 6a3b39b428 fix context menu in disabled state 2017-07-26 09:07:20 +02:00
Joao Moreno 9e80a12462 fix tests 2017-07-25 17:17:48 +02:00
Joao Moreno 0e0ecc2d6a listen to shift when in Linux 2017-07-25 17:11:41 +02:00
Joao Moreno f872ced863 oops, this is a function 2017-07-25 11:30:03 +02:00
Joao Moreno 9a30b98880 safeguard npe 2017-07-25 10:45:45 +02:00
Daniel Imms f36bd24e1e Merge pull request #817 from Lixire/master
Add lineFeed event
2017-07-24 14:58:32 -07:00
t-amqi 23dd3d7a67 Fix indentation 2017-07-24 13:41:24 -07:00
t-amqi 79965708ab Add lineFeed event
- this is useful for checking when a cmd is entered or when a cmd finishes
- also handy to have for accessibility for output
2017-07-24 11:43:03 -07:00
Joao Moreno 80a83ff8d9 wip: alt forces selection 2017-07-24 18:08:11 +02:00
Daniel Imms 2e9177a032 Merge pull request #810 from Tyriar/809_onSingleClick_null_check
Make SelectionManager more resilient
2017-07-21 06:30:56 -07:00
Daniel Imms bbb5daa575 Merge branch 'master' into 809_onSingleClick_null_check 2017-07-21 06:22:43 -07:00
Daniel Imms 739bd05037 Merge pull request #811 from blink1073/add-jupyter
Add JupyterLab as a real-world use case
2017-07-21 06:17:56 -07:00
Steven Silvester f7197c10df Add JupyterLab as a real-world use case 2017-07-21 07:59:28 -05:00
Daniel Imms e56f710cd1 Make SelectionManager more resilient
Fixes #809
2017-07-21 05:45:53 -07:00
Daniel Imms 18f3cc77e5 Merge pull request #805 from Tyriar/801_null_check
Add null check when mouse coords are bad
2017-07-20 09:19:09 -07:00
Daniel Imms e3ded61122 Add null check when mouse coords are bad
This happens if the viewport is bad (which was fixed in another commit).

Fixes #801
2017-07-18 20:34:24 -07:00
Daniel Imms f8e3fa5a99 Fix browserify build and search addon (#804)
Fixes #803
2017-07-18 19:58:35 -07:00
Daniel Imms 05d308a37a Merge pull request #798 from jerch/faster_wcwidth
faster wcwidth with lookup table
2017-07-18 14:23:42 -07:00
Jörg Breitbart cd5827e69b higher timeout for test case 2017-07-18 22:19:55 +02:00
Daniel Imms 20048d31a1 Merge branch 'master' into faster_wcwidth 2017-07-17 11:06:38 -07:00
Jörg Breitbart 598a729fc0 fixes #799 - lookup table is created on first wcwidth call for char > 127 2017-07-17 19:07:02 +02:00
Jörg Breitbart 6ed5c9e64d speed test commented out 2017-07-16 23:53:41 +02:00
Jörg Breitbart 1232c00dd1 some fixes
- commenting the bit shifts
- switching to Uint32Array
- conditional for TypedArray
- fixing Math.floor slowdown in bisearch
2017-07-16 23:40:36 +02:00
Jörg Breitbart 5d87b54390 test speedup defensive 2017-07-16 14:09:25 +02:00
Jörg Breitbart 6733c6ce1f test speedup defensive 2017-07-16 14:05:55 +02:00
Jörg Breitbart ba2d0e8df2 fixing whitespaces 2017-07-16 13:57:04 +02:00
Jörg Breitbart 1722248065 fixing whitespaces 2017-07-16 13:48:48 +02:00
Jörg Breitbart dd0fcb6320 fixing whitespaces 2017-07-16 13:44:00 +02:00
Jörg Breitbart b965d0a0af faster wcwidth with lookup table 2017-07-16 13:26:38 +02:00
Paris Kasidiaris 48dab494db Install npm 5.1.0 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 42eacba09a Update build dir 2017-07-16 03:47:15 +03:00
Paris Kasidiaris e2c993ebcd Rearrange dependencies 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 634f4e29aa Remove redundant comment 2017-07-16 03:47:15 +03:00
Daniel Imms 59f75555bc Add IBuffer, IBufferSet and fix CircularList type 2017-07-16 03:47:15 +03:00
Paris Kasidiaris cb5a452c90 Implement tests for Buffer and BufferSet 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 58b9f712df Improve documentation of Buffer and BufferSet classes 2017-07-16 03:47:15 +03:00
Paris Kasidiaris f09918f0d2 Fix variable name 2017-07-16 03:47:15 +03:00
Aleksandr Andriienko 4e35904392 Fix regression. Existing tests work again.
Signed-off-by: Aleksandr Andriienko <oandriie@redhat.com>
2017-07-16 03:47:15 +03:00
Paris Kasidiaris 82b3beb921 Fix reference to terminal.buffer.lines 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 2806288489 Add semicolons 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 0d498fcd52 Correct references to buffer lines 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 83efc9afc4 Fix SelectionManager tests 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 393b411acc Fix CircularList type and a typo 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 4effe1f1d9 Add buffers to the ITerminal 2017-07-16 03:47:15 +03:00
Oleksandr Andriienko de82bd00ae Fix some tests and docs, little code fix up.
Fix some tests and docs. Add ability to run one test file with help gulp task.

Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2017-07-16 03:47:15 +03:00
Paris Kasidiaris d222eec9f7 Fix SelectionManager initializing 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 2cda163bcf Fix forgotten merge conflict 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 9025d91f12 Fix first set of existing tests 2017-07-16 03:47:15 +03:00
Aleksandr Andrienko 6810de85e1 Fix turning from alt screen to normal screen and vice versa. Fix https://github.com/sourcelair/xterm.js/issues/229 . 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 4626e19b75 Move scrollTop and scrollBottom into Buffer 2017-07-16 03:47:15 +03:00
Paris Kasidiaris d75e0b99d5 Fix more references to buffer.x 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 73b4bfb5f8 Fix some references 2017-07-16 03:47:15 +03:00
Paris Kasidiaris bbafdd3d06 Start isolating buffer attributes into Buffer class 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 8ede1fc9d2 Create terminal.buffer convenience attribute 2017-07-16 03:47:15 +03:00
Paris Kasidiaris 95cb6f30bb Introduce new Buffer and BufferSet classes 2017-07-16 03:47:15 +03:00
Daniel Imms c94fdaecb2 Merge pull request #797 from mofux/guard_selection_api
Guard public selection api
2017-07-14 10:31:25 -07:00
Daniel Imms e2b43a4ced Merge branch 'master' into guard_selection_api 2017-07-14 09:51:58 -07:00
Daniel Imms 1fd087e5d3 Merge pull request #795 from mofux/remove_unused_vars
Remove unused constants from the SelectionManager
2017-07-14 09:51:12 -07:00
mofux 6df6aad303 Improve guard expression for hasSelection 2017-07-14 18:41:55 +02:00
Daniel Imms 5140c83649 Merge branch 'master' into remove_unused_vars 2017-07-14 09:23:33 -07:00
mofux 1b6ce6dab6 Make sure selection manager is available (only after open() was called) 2017-07-14 18:40:32 +03:00
mofux 1cc0d43aa4 Clear selection on input 2017-07-14 18:40:32 +03:00
mofux 0af4b17c20 Guard public selection api 2017-07-14 15:56:14 +02:00
mofux 2275601bf7 Removed unused constants from the selection manager 2017-07-14 14:22:30 +02:00
Daniel Imms 7cfc2ffa32 Merge pull request #793 from Tyriar/bad_getOption_param
Remove bad getOption param
2017-07-13 12:45:18 -07:00
Daniel Imms 9e95024a9d Merge branch 'master' into bad_getOption_param 2017-07-13 11:47:41 -07:00
Daniel Imms 87b76aadb7 Merge pull request #704 from Tyriar/553_find_api
Implement very basic find API
2017-07-13 11:12:45 -07:00
Daniel Imms adc657898c Remove bad getOption param 2017-07-13 09:23:56 -07:00
Daniel Imms 43b47abaa7 Merge branch 'master' into 553_find_api 2017-07-13 08:51:11 -07:00
Daniel Imms 2ebc926f4e Merge pull request #761 from Tyriar/760_erase_in_display_scroll_bar
Ensure scroll bar is refreshed after erase all in display
2017-07-13 08:48:09 -07:00
Daniel Imms af58643ee7 Merge branch 'master' into 553_find_api 2017-07-13 08:37:04 -07:00
Daniel Imms 9a358b2258 Merge branch 'master' into 760_erase_in_display_scroll_bar 2017-07-13 07:56:19 -07:00
Daniel Imms 25fc01cb81 Merge pull request #780 from mofux/mousedown_counter
[SelectionManager] Use event.detail instead for counting the mousedown event
2017-07-12 19:07:55 -07:00
Thomas Zilz d3fe3a1960 Merge branch 'master' into mousedown_counter 2017-07-13 00:08:55 +02:00
Paris Kasidiaris 01453e9d40 Merge pull request #784 from chrmarti/chrmarti/20505
Check if we still need to measure the char size
2017-07-12 11:59:37 +03:00
Christof Marti 27f0eaf71d Check if we still need to measure the char size (fixes #785) 2017-07-11 15:48:08 -07:00
thomas 9bd52820e7 use evt.detail as mouse counter 2017-07-11 20:13:13 +02:00
Paris Kasidiaris 86c5aa023f Merge pull request #772 from mjbvz/add-search-exclude-for-vscode
Add search.exclude setting for vscode workspace
2017-07-11 20:58:47 +03:00
Paris Kasidiaris 4df8d3aada Merge branch 'master' into add-search-exclude-for-vscode 2017-07-11 20:30:56 +03:00
Daniel Imms 8fab079ad6 Merge pull request #779 from stuicey/master
Add SSHy as real-world use
2017-07-11 09:55:11 -07:00
stuicey e59d1f7a98 Add SSHy as real-world use 2017-07-11 16:08:39 +01:00
Paris Kasidiaris 5f817e88e7 Merge pull request #775 from deviantony/usage-portainer
Add Portainer.io as a real world use case
2017-07-11 10:59:35 +03:00
Anthony Lapenna 38de6cb46a Update Portainer URL 2017-07-11 09:49:22 +02:00
Anthony Lapenna e287edb6fe Add Portainer.io as a real world use case 2017-07-11 09:48:11 +02:00
Matt Bierner 29545a90c6 Add search.exclude setting for vscode workspace 2017-07-10 14:46:25 -07:00
Daniel Imms e849e3f206 Merge pull request #766 from Tyriar/765_scroll_needlessly
Don't refresh terminal when scrollDisp doesn't scroll
2017-07-10 07:31:47 -07:00
Daniel Imms c3011cd3ea Merge branch 'master' into 765_scroll_needlessly 2017-07-09 16:40:55 -07:00
Daniel Imms e4770de9f2 Merge branch 'master' into 760_erase_in_display_scroll_bar 2017-07-09 16:40:47 -07:00
Paris Kasidiaris 99061c672c Merge pull request #770 from lanwen/patch-1
Real world case: Selenoid UI
2017-07-09 14:02:47 +03:00
Kirill Merkushev 27fcd7c260 Real world case: Selenoid UI
http://aerokube.com/selenoid-ui/latest/#_logs
https://github.com/aerokube/selenoid-ui/blob/master/web/src/components/Log/index.js
2017-07-09 13:18:03 +03:00
Daniel Imms 4c0e7b2462 Merge remote-tracking branch 'ups/master' into 553_find_api 2017-07-09 01:57:04 -07:00
Daniel Imms decd81bc1e Merge pull request #690 from Tyriar/687_macos_ci
Skip failing tests of macOS and add to CI
2017-07-09 01:54:58 -07:00
Daniel Imms 73970ec5a2 Fix casing of search.js 2017-07-09 01:49:17 -07:00
Daniel Imms c62923fb1d Change case of browserify-addons files 2017-07-09 01:09:44 -07:00
Daniel Imms c13b86a885 Add find to demo 2017-07-09 01:03:10 -07:00
Daniel Imms 2564592c09 Change case of search addon files 2017-07-09 00:55:50 -07:00
Daniel Imms 4b0cf2ffce jsdoc 2017-07-09 00:43:31 -07:00
Daniel Imms fab039ddf3 Rearrange module loader to work with browserify 2017-07-09 00:26:33 -07:00
Daniel Imms 3c0e142f73 Fix sourcemaps 2017-07-08 23:53:16 -07:00
Daniel Imms 80f7a37118 Point to search addon properly 2017-07-08 23:13:41 -07:00
Daniel Imms 11de0d0ca0 Merge remote-tracking branch 'ups/master' into 553_find_api 2017-07-08 23:07:57 -07:00
Daniel Imms 5e815de82a Remove comments from Travis config 2017-07-08 19:13:43 -07:00
Daniel Imms 0536862b8f Try fix build 2017-07-08 17:47:54 -07:00
Daniel Imms b520e2fd35 Use standard naming for functions 2017-07-08 17:33:18 -07:00
Daniel Imms 740d4a45f9 Remove dependency on sleep module 2017-07-08 17:32:47 -07:00
Daniel Imms cb53cdd728 Merge remote-tracking branch 'ups/master' into 687_macos_ci 2017-07-08 17:19:47 -07:00
Paris Kasidiaris 2b92996d7c Bump version to 2.8.1
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-07-08 14:02:43 +00:00
Paris Kasidiaris b8944beea9 Merge pull request #768 from Tyriar/767_initial_viewport_wrap
Properly mark initial viewport lines as wrapped
2017-07-08 16:49:46 +03:00
Paris Kasidiaris 67435edad7 Merge branch 'master' into 767_initial_viewport_wrap 2017-07-08 16:45:47 +03:00
Paris Kasidiaris fc9a0f15db Merge pull request #769 from sourcelair/fix-tests
Update jsdom to 11.1
2017-07-08 16:44:57 +03:00
Paris Kasidiaris 8a57489966 Add missing semicolon 2017-07-08 16:40:18 +03:00
Paris Kasidiaris 3a8c1b3fcd Update copyright note 2017-07-08 16:36:19 +03:00
Paris Kasidiaris 0c2f079670 Update gulpfile.js
The `outDir` compiler option of TypeScript seems to be an absolute URL now.
2017-07-08 16:35:53 +03:00
Paris Kasidiaris d266d995ab Fix typo 2017-07-08 11:13:06 +03:00
Paris Kasidiaris 0ea2e9cfd6 Update jsdom to 1.11
Also, add package-lock.json
2017-07-08 10:59:38 +03:00
Daniel Imms 9b8cb03c55 Properly mark initial viewport lines as wrapped
Fixes #767
2017-07-07 11:18:18 -07:00
Daniel Imms 729f646a28 Use emit directly due to #766 2017-07-06 22:28:33 -07:00
Daniel Imms 85c892c218 Don't refresh terminal when scrollDisp doesn't scroll
Fixes #765
2017-07-06 21:16:20 -07:00
Daniel Imms 7f5e72dbdb Ensure scroll bar is refreshed after erase all in display
Fixes #760
2017-07-05 13:06:10 -07:00
Paris Kasidiaris 479fb0e782 Bump version to 2.8.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-07-04 16:45:20 +00:00
Paris Kasidiaris 8ba11a2bd0 Merge pull request #752 from LucianBuzzo/751-IE11-demo
Add a Promise polyfill to the demo so that it works in IE11
2017-07-04 12:53:13 +03:00
Paris Kasidiaris abc3f3b94d Merge branch 'master' into 751-IE11-demo 2017-07-04 12:50:37 +03:00
Daniel Imms 67bc0456e0 Merge pull request #747 from anishathalye/master
Add scroll functionality for mobile
2017-07-03 21:48:00 -07:00
Daniel Imms 3e8ddd1966 Add jsdoc 2017-07-03 21:43:52 -07:00
Anish Athalye 6223342890 Add scroll functionality for mobile 2017-07-03 19:49:16 -07:00
Daniel Imms 9ddabb3fee Merge pull request #750 from irokas/Rows-fix
Set minimum scrollback value to number of rows if needed on resize
2017-07-03 18:23:48 -07:00
Daniel Imms af0030b580 Merge branch 'master' into Rows-fix 2017-07-03 18:14:31 -07:00
Paris Kasidiaris b93271d9cd Merge branch 'master' into 751-IE11-demo 2017-07-03 14:55:48 +03:00
Paris Kasidiaris 934ca6508f Merge pull request #754 from LucianBuzzo/753-ie11-terminal-cursor
Add one class at a time to element for IE11 compatibility
2017-07-03 14:55:01 +03:00
Lucian Buzzo 7e210c7e6b Add one class at a time to element for IE11 compatibility
Connects to #753

Element.classList.add() does not work with multiple arguments in IE.
2017-07-03 11:22:37 +01:00
Lucian Buzzo e4970266d8 Add a Promise polyfill to the demo so that it works in IE11
Connects to #751
2017-07-03 11:05:35 +01:00
irokas 0a5d7a7bd2 Set minimum scrollback value to number of rows if needed on resize 2017-07-02 21:10:18 +00:00
Daniel Imms 31aab8ea6f Merge pull request #746 from Tyriar/745_colon_word_sep
Remove colon from the selection word separators
2017-07-02 12:36:46 -07:00
Daniel Imms 857d9bcf2a Merge branch 'master' into 745_colon_word_sep 2017-07-02 12:30:45 -07:00
Daniel Imms 7463166aec Fix tests 2017-07-02 12:29:31 -07:00
Paris Kasidiaris f80159fedc Merge pull request #749 from irokas/fix-scrollback
Fix issue with small scrollback value
2017-07-02 14:39:07 +03:00
irokas 3180a6f6ec Add test for scrollback fix 2017-07-02 14:34:19 +03:00
irokas 35d26f1d27 Fix issue with small scrollback value.
Closes #506
2017-07-02 14:22:24 +03:00
Daniel Imms ca03f8928d Remove colon from the word separators
Fixes #745
2017-06-30 17:07:52 -07:00
Daniel Imms 0b3d1e50a8 Merge pull request #741 from AndrienkoAleksandr/codeCleanUp
Little code clean up.
2017-06-30 09:50:54 -07:00
Daniel Imms 401886c7c3 Merge branch 'master' into codeCleanUp 2017-06-30 09:46:54 -07:00
Daniel Imms 4eac7661c2 Merge pull request #733 from Tyriar/732_drop_selection_on_alt
Drop selection when the active buffer changes
2017-06-30 09:40:49 -07:00
Daniel Imms b8163cfbbf Merge branch 'master' into 732_drop_selection_on_alt 2017-06-30 09:36:26 -07:00
Paris Kasidiaris a9f82907ee Merge pull request #743 from nechitai/add-user
Add hexlet.io to the users list
2017-06-30 15:40:30 +03:00
Artem Arbatskiy 57f4ae5320 Add hexlet.io to the users list 2017-06-30 18:30:02 +07:00
Aleksandr Andriienko d1712424c8 Fix docs. 2017-06-30 09:57:24 +03:00
Oleksandr Andriienko 16f865987d Little code clean up. 2017-06-29 23:36:07 +03:00
Daniel Imms f071531494 Merge branch 'master' into 732_drop_selection_on_alt 2017-06-29 12:13:10 -07:00
Daniel Imms 618768c7ed Merge pull request #738 from Tyriar/737_crlf_windows
Use crlf when copying on Windows
2017-06-29 12:12:56 -07:00
Daniel Imms cfe22e6fd2 Use crlf when copying on Windows
Fixes #737
2017-06-29 07:38:41 -07:00
Daniel Imms 3961dd5a44 Merge branch 'master' into 732_drop_selection_on_alt 2017-06-28 15:21:27 -07:00
Daniel Imms 471e332036 Merge pull request #736 from AndrienkoAleksandr/master
Fix 'CR' issue on Linux based OS.
2017-06-28 12:45:16 -07:00
Oleksandr Andriienko 467e8c563a Fix up. 2017-06-28 00:48:13 +03:00
Oleksandr Andriienko 5971630f64 Simplify .gitattributes
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2017-06-28 00:39:16 +03:00
Oleksandr Andriienko f7cc1058c7 Fix 'CR' issue on Linux based OS.
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2017-06-27 23:25:01 +03:00
Daniel Imms 645a5920da Drop selection when the active buffer changes
Fixes #732
2017-06-27 10:13:16 -07:00
Daniel Imms baccca843c Merge branch 'master' into 553_find_api 2017-06-22 19:37:18 -07:00
Daniel Imms 19381454ad Get find working as an addon 2017-06-22 19:36:53 -07:00
Paris Kasidiaris f95bc64971 Merge pull request #712 from chabou/customKeypressHandler
Add attachCustomKeyEventsHandler
2017-06-22 16:16:23 +03:00
Daniel Imms 0c7c307de3 Merge branch 'master' into customKeypressHandler 2017-06-21 08:51:15 -07:00
Daniel Imms 1c84881d00 Merge pull request #725 from Tyriar/724_fix_hasSelection
Have Terminal.hasSelection return false when no selection
2017-06-21 08:38:43 -07:00
Daniel Imms f1252abbbe Merge branch 'master' into 724_fix_hasSelection 2017-06-21 08:34:33 -07:00
Daniel Imms 11d6ca2aa1 Merge pull request #723 from Tyriar/699_linux_middle_click
Fix select and middle click to paste selection
2017-06-21 08:34:06 -07:00
Daniel Imms eb59b90790 Merge branch 'master' into 699_linux_middle_click 2017-06-21 08:29:54 -07:00
Daniel Imms 4c3aa97917 Merge pull request #719 from Tyriar/718_selection_on_top
Render selection on top of rows
2017-06-21 08:29:40 -07:00
Daniel Imms ba54f25b8c Merge branch 'master' into 718_selection_on_top 2017-06-21 08:25:05 -07:00
Daniel Imms af268f4968 Merge pull request #722 from Tyriar/721_firefox_contextmenu
Fix context menu in firefox
2017-06-21 08:24:52 -07:00
Daniel Imms f93b24acc7 Merge branch 'master' into 553_find_api 2017-06-20 17:44:43 -07:00
Daniel Imms 6854def13c Use case-insensitive search by default 2017-06-20 17:40:31 -07:00
Daniel Imms 63d63c27e7 Ensure outside selection pastes are respected
We were resetting the selection on every auxclick, meaning outside
selections could not be pasted in when the terminal had a current
selection.
2017-06-20 11:17:12 -07:00
Daniel Imms ef1e2ab9e2 Use auxclick event 2017-06-20 10:49:58 -07:00
Daniel Imms 7b46940772 Have Terminal.hasSelection return false when no selection
Fixes #724
2017-06-20 10:38:33 -07:00
Daniel Imms 6f5f68df1a Add a comment 2017-06-20 10:24:16 -07:00
Daniel Imms dc165175cf Handle middle click to paste selection
This appears to work in Chrome but not Firefox
2017-06-20 10:18:29 -07:00
Daniel Imms 32f1782c84 Fix context menu in firefox
Fixes #721
2017-06-20 10:01:39 -07:00
Daniel Imms 8811d96a8b Fix selection going to clipboard
Part of #699
2017-06-19 15:45:18 -07:00
CHaBou d11d74688e Merge branch 'master' into customKeypressHandler 2017-06-19 22:10:03 +02:00
CHaBou 8e79d53d78 Add attachCustomKeyEventHandler()
Use a uniq customKeyEventHandler for keyDown and keyPress events
Mark and warn attachCustomKeydownHandler() as deprecated.
2017-06-19 22:03:19 +02:00
Daniel Imms 6dbc476cdc Allow mouse events to pass through selection 2017-06-19 11:26:52 -07:00
Daniel Imms 57921e3fe0 Render selection on top of rows
Fixes #718
2017-06-19 11:18:55 -07:00
Daniel Imms 75b74ac4ff Merge pull request #716 from Tyriar/697_select_to_word
Select to the word at the mouse after double click
2017-06-19 07:56:35 -07:00
Daniel Imms 1c659841fa Merge branch 'master' into 697_select_to_word 2017-06-19 07:47:40 -07:00
Daniel Imms 9573357778 Merge pull request #715 from Tyriar/709_improve_selection
Improve selection cell selection
2017-06-19 07:47:15 -07:00
Daniel Imms f11362a80d Merge branch 'master' into 709_improve_selection 2017-06-19 07:39:29 -07:00
Daniel Imms a01fbb636a Merge pull request #714 from Tyriar/713_dblclick_word_separators
Don't select colon, quotes or brackets when double clicking
2017-06-19 07:39:15 -07:00
Daniel Imms 320fb55ddc Use an enum for selection mode 2017-06-18 12:56:32 -07:00
Daniel Imms 4b170ca43f Select to the word at the mouse after double click
Fixes #697
2017-06-18 12:52:43 -07:00
Daniel Imms c6e7fb74ab Merge branch 'master' into 709_improve_selection 2017-06-18 12:03:30 -07:00
Daniel Imms b05814e6a5 Improve selection cell selection
Fixes #709
2017-06-18 12:02:58 -07:00
Daniel Imms 07882c42dc Also consider ' and " word separators
For example URIs in json
2017-06-18 11:51:00 -07:00
Daniel Imms a59b2f8433 Merge branch 'master' into 713_dblclick_word_separators 2017-06-18 11:08:22 -07:00
Daniel Imms 7f2e94c30c Don't select colon or brackets when double clicking
Fixes #713
2017-06-18 10:38:04 -07:00
CHaBou 95aed8fcb0 Add customKeypressHandler 2017-06-16 20:23:48 +02:00
Daniel Imms 663d3b24cd Merge pull request #711 from sourcelair/Tyriar-patch-1
Move latest addition to real world use cases to the end
2017-06-16 07:17:39 -07:00
Daniel Imms a87303f375 Move latest addition to real world use cases to the end
Seems a little unfair to not be at the end, to date it's sorted by date added.
2017-06-16 07:13:03 -07:00
Paris Kasidiaris 259858fac2 Merge pull request #710 from alex-kovoy/alex-kovoy-patch-1
Added 'Gravitational Teleport' to Real-world uses
2017-06-15 23:15:04 +01:00
Alexey Kontsevoy a4514ee98a Added 'Gravitational Teleport' to Real-world uses
We use xterm.js in Gravitational Teleport.
2017-06-15 18:05:07 -04:00
Daniel Imms 39151e4354 Merge branch 'master' into 553_find_api 2017-06-15 09:11:32 -07:00
Daniel Imms d27b7bfa7a Merge pull request #707 from jdanyow/issue-683
normalize line endings to \r
2017-06-14 15:22:04 -07:00
Jeremy Danyow ce61795c38 normalize line endings to \r
fixes #683
2017-06-14 14:51:36 -07:00
Daniel Imms 528f06bc62 Remove log 2017-06-13 20:03:29 -07:00
Daniel Imms 5360277594 Merge remote-tracking branch 'origin/master' into 553_find_api 2017-06-13 19:57:52 -07:00
Daniel Imms b957130787 Consolidate translateBufferLineToString functions 2017-06-13 19:55:29 -07:00
Daniel Imms f88dcd160c Expose API on Terminal 2017-06-13 19:49:12 -07:00
Daniel Imms 7d8f698096 Add findPrevious 2017-06-13 19:43:18 -07:00
Daniel Imms d3f5a06517 Fix search scrolling up 2017-06-13 19:34:25 -07:00
Daniel Imms 9cc878cea7 Merge pull request #688 from Tyriar/642_link_wrapped_char_duplication
Fix issue with strings in wrapped spans being duplicated
2017-06-13 14:53:23 -07:00
Daniel Imms 087acf8a9a Merge branch 'master' into 642_link_wrapped_char_duplication 2017-06-13 14:45:17 -07:00
Daniel Imms 7612180276 Merge pull request #691 from Tyriar/mid_selection_single_element
Use a single element for the middle of the selection
2017-06-13 14:45:11 -07:00
Daniel Imms 201e3e4dab Merge branch 'master' into mid_selection_single_element 2017-06-13 14:40:48 -07:00
Daniel Imms 55f51ad169 Merge pull request #693 from Tyriar/443_retain_wrapped_lines_copy
Retain wrapped line state when copying
2017-06-13 14:40:41 -07:00
Daniel Imms 405d1ee52d Merge branch 'master' into 443_retain_wrapped_lines_copy 2017-06-13 14:32:01 -07:00
Daniel Imms 5f9ad5c19e Merge pull request #700 from Tyriar/698_mousedown_preventDefault
Prevent mousedown event default selection behavior
2017-06-13 14:31:50 -07:00
Daniel Imms 280b698a50 More work on findNext 2017-06-12 21:59:46 -07:00
Daniel Imms 8d12881a1c Very basic find implementation
Part of #553
2017-06-12 16:27:13 -07:00
Daniel Imms c5cfa409b1 Merge branch 'master' into 698_mousedown_preventDefault 2017-06-12 14:53:48 -07:00
Daniel Imms 2a0f44b0fb Prevent mousedown event default selection behavior
Fixes #698
2017-06-12 14:51:58 -07:00
Daniel Imms 7d0e585c44 Merge branch 'master' into 642_link_wrapped_char_duplication 2017-06-11 11:34:22 -07:00
Daniel Imms a5ced5d909 Merge branch 'master' into mid_selection_single_element 2017-06-11 11:34:20 -07:00
Daniel Imms 0a99f65f01 Merge branch 'master' into 443_retain_wrapped_lines_copy 2017-06-11 11:34:17 -07:00
Daniel Imms c33814a7a6 Merge pull request #680 from Tyriar/set_row_height_explicitly
Set row height when char size is changed
2017-06-10 10:25:06 -07:00
Daniel Imms 0950369a5c Merge remote-tracking branch 'origin/master' into set_row_height_explicitly 2017-06-10 10:21:20 -07:00
Daniel Imms 346b5177bc Retain wrapped line state when copying
Fixes #443
2017-06-09 17:12:00 -07:00
Daniel Imms 9246d524cb Use a single element for the middle of the selection
This reduces the number of DOM nodes used for the selection dramatically.
2017-06-09 16:35:26 -07:00
Daniel Imms 6480933861 Skip failing tests of macOS and add to CI
Fixes #687
2017-06-09 16:27:47 -07:00
Daniel Imms a889fef752 Merge pull request #670 from Tyriar/207_selection_manager
Reimplement selection in the terminal
2017-06-09 13:41:34 -07:00
Daniel Imms 6075498c24 Add class header comments 2017-06-09 12:47:16 -07:00
Daniel Imms ee1da73a69 Add regression test 2017-06-09 11:59:18 -07:00
Daniel Imms 29ac6a153e Fix issue with strings in wrapped spans being duplicated
This was happening because the linkifier assumed that top-level spans
in the terminal would only contain single text nodes

Fixes #642
2017-06-09 11:48:12 -07:00
Daniel Imms 264757e964 Use user-select supported by other browsers 2017-06-07 19:23:46 -07:00
Daniel Imms e88a9ffd89 Fix bug when selection end is less than start 2017-06-07 19:17:38 -07:00
Daniel Imms d56a611a19 Add .gitattributes file
Makes working on Windows less bothersome.
2017-06-07 19:16:59 -07:00
Daniel Imms 28ed7772c6 Select last character in bottom right in select all 2017-06-07 14:44:16 -07:00
Daniel Imms a047359f81 Add more selection manager/model tests 2017-06-07 14:37:09 -07:00
Daniel Imms b81c165bea Add SelectionModel tests 2017-06-07 14:15:18 -07:00
Daniel Imms 4405f5e163 Fix clearSelection in select all mode 2017-06-07 13:48:12 -07:00
Daniel Imms 2012c8c91d Move prepareTextForClipboard logic into SelectionManager 2017-06-07 13:42:08 -07:00
Daniel Imms f719a4e91b Fix tests 2017-06-07 13:20:44 -07:00
Daniel Imms 5d33727ae4 Add clearSelection API 2017-06-07 13:07:34 -07:00
Daniel Imms 1343b83fc3 Add hasSelection public API 2017-06-07 12:40:48 -07:00
Daniel Imms 9e47ec9af2 Add getSelectionText API
This allows consumers to implement their own copy functionality/keybindings
2017-06-07 11:59:28 -07:00
Daniel Imms 9c459aacc7 Explicitly size selectionContainer to allow for more flexible layouts 2017-06-07 11:48:31 -07:00
Daniel Imms 72724abac8 Ensure host program handles copy if mouseevents are active 2017-06-07 11:20:03 -07:00
Daniel Imms 9a86eeb333 Support copying in the alt buffer 2017-06-07 11:10:05 -07:00
Daniel Imms 46f6fd18b0 Merge remote-tracking branch 'ups/master' into set_row_height_explicitly 2017-06-06 14:34:07 -07:00
Daniel Imms 603f22a7bb Set row height when char size is changed
When zoomed in line-height and actual rendered row height differs. This
can be seen in the viewport being too large when zoomed in right now
but also has additional impacts in the new selection model #207
2017-06-06 14:31:57 -07:00
Daniel Imms 13c401cbaa Only refresh the selection on an animation frame 2017-06-06 10:52:42 -07:00
Daniel Imms b812991072 Fix multiple drag scroll intervals being registered 2017-06-06 10:40:22 -07:00
Daniel Imms 2b2431824f Fix select all with no start and include content below viewport 2017-06-06 23:13:50 -07:00
Daniel Imms 7147787441 Undo CiruclarList id changes, fix null check, lint 2017-06-06 22:59:16 -07:00
Daniel Imms ec3bf1139d Resolve TODOs 2017-06-06 22:33:32 -07:00
Daniel Imms d0b603d033 jsdoc all of SelectionManager 2017-06-06 21:55:52 -07:00
Daniel Imms 2621be819e Resolve more TODOs, add jsdoc 2017-06-06 21:43:19 -07:00
Daniel Imms f380153f42 Resolve some TODOs 2017-06-06 21:29:30 -07:00
Daniel Imms fd91c5e1a8 Add _selectLineAt test 2017-06-06 15:22:35 -07:00
Daniel Imms f61d852f3c Don't allow double click selection on empty row going out of viewport 2017-06-06 15:05:22 -07:00
Daniel Imms 5bc1112151 Select lines when dragging a triple click 2017-06-06 15:02:52 -07:00
Daniel Imms 59cc0b675e Merge remote-tracking branch 'ups/master' into 207_selection_manager 2017-06-06 14:35:06 -07:00
Daniel Imms db8ded2a02 Implement shift+click 2017-06-06 14:19:12 -07:00
Paris Kasidiaris 4b7279146a Bump version to 2.7.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-06-06 15:04:59 +00:00
Paris Kasidiaris c4a2f7911d Merge pull request #674 from sourcelair/issue-#671-restore-paste-event
Restore the "paste" terminal event
2017-06-06 11:23:18 +03:00
Paris Kasidiaris db9fe53af3 Merge branch 'master' into issue-#671-restore-paste-event 2017-06-06 11:16:16 +03:00
Daniel Imms d3865ad23f Add some SelectionManager selectWorkAt tests, fix lint 2017-06-01 17:36:47 -07:00
Daniel Imms 8b1067d180 Remove no longer valid clipboard test and add new one 2017-06-01 17:11:07 -07:00
Daniel Imms a53a0acd65 Fix issue with trimming whitespace 2017-06-01 17:04:59 -07:00
Daniel Imms 42724c7f42 Merge pull request #672 from Tyriar/662_parser_state_hack
Fix bug with parser state when using vtop
2017-05-27 15:07:52 -07:00
Paris Kasidiaris 616f069538 Fix #671 - Restore paste event 2017-05-27 15:30:26 +03:00
Daniel Imms 11cec31da6 Clean up wide char select word code 2017-05-25 21:12:44 -07:00
Daniel Imms d9991b25a4 Support wide char double click 2017-05-25 20:48:48 -07:00
Daniel Imms cb6533f3a6 Get double click selection for wide chars mostly working 2017-05-25 20:22:43 -07:00
Daniel Imms 54e7f65d4e Support selecting wide characters 2017-05-25 19:34:32 -07:00
Daniel Imms ec61f3acf0 Only trim the right whitespace from selection
Fixes #673
2017-05-25 18:52:06 -07:00
Daniel Imms 910aa1ad7f Properly prepare text for clipboard for context menu
This makes the context menu in macOS embed the right text
2017-05-25 18:12:03 -07:00
Daniel Imms 24bed01fa4 Support copy and paste via context menu 2017-05-25 16:17:43 -07:00
Daniel Imms f7d6ab5f2a Move the selection model to its own module 2017-05-25 13:20:57 -07:00
Daniel Imms 0716cffd7d Tidy up _areSelectionValuesReversed 2017-05-25 12:46:54 -07:00
Daniel Imms e29ab29441 Fix drag when selectioning via double/triple click 2017-05-25 12:46:08 -07:00
Daniel Imms 9f271de803 Implement triple click to select line 2017-05-25 12:27:47 -07:00
Daniel Imms 43c796a781 Support copying of select all text 2017-05-25 11:35:22 -07:00
Daniel Imms 25152e4441 Add select all API 2017-05-25 10:14:40 -07:00
Daniel Imms 3846fe0a7d Add null checks to fix trim related crash 2017-05-25 09:55:08 -07:00
Daniel Imms b3b2bd1faf Don't flag user scrolling if no scrolling happens 2017-05-25 09:51:13 -07:00
Daniel Imms 2c3d98abc8 Fix bug with parser state when using vtop
Fixes #662
2017-05-25 09:36:59 -07:00
Daniel Imms 0dc3dd0336 Initial scroll drag implementation 2017-05-23 11:20:55 -07:00
Daniel Imms c4762e7228 Merge remote-tracking branch 'ups/master' into 207_selection_manager 2017-05-22 19:51:42 -07:00
Daniel Imms 35b32b721e Merge pull request #668 from Tyriar/fix_pt_valid_var_declarations
Declare pt and valid vars in correct spot
2017-05-22 08:19:52 -07:00
Daniel Imms ccf1a047d3 Merge branch 'master' into fix_pt_valid_var_declarations 2017-05-22 08:03:53 -07:00
Daniel Imms db0338cbcf Merge pull request #669 from Tyriar/pull_getCoords_into_module
Pull get coords into module
2017-05-22 08:03:31 -07:00
Daniel Imms 06742eb507 Merge branch 'master' into pull_getCoords_into_module 2017-05-22 07:57:55 -07:00
Daniel Imms 0fab4ff5d1 Merge pull request #667 from Tyriar/add_missing_emit_arg
Add missing EventEmitter.emit args/types
2017-05-22 07:57:35 -07:00
Daniel Imms d2b0d514f8 Add missing semi-colons 2017-05-21 13:51:02 -07:00
Daniel Imms ab40908fa4 Disable selection manager in mouse mode
This allows the application to take full control of the selection
2017-05-21 12:35:07 -07:00
Daniel Imms a5c48c891e Temp fix until mouse getCoords PR merged 2017-05-21 12:16:36 -07:00
Daniel Imms 70e35de56b Polish comments and cap code 2017-05-21 12:05:30 -07:00
Daniel Imms c323e0ad4a jsdoc 2017-05-21 11:57:24 -07:00
Daniel Imms 4d32826891 Pull getCoords into a Mouse util module
This is part of the work to prepare for the upcoing selection changes

Related #207
2017-05-21 11:50:01 -07:00
Daniel Imms 13088f55a0 Declare pt and valid vars in correct spot 2017-05-21 11:27:57 -07:00
Daniel Imms ea008411e4 Add missing EventEmitter.emit args/types 2017-05-21 11:25:18 -07:00
Daniel Imms e63fdf580b Clean up 2017-05-20 15:21:12 -07:00
Daniel Imms 597c6939c8 Handle basic double click select 2017-05-20 00:17:23 -07:00
Daniel Imms 293ae18a11 Fix copy after scroll 2017-05-20 00:01:55 -07:00
Daniel Imms 32b34cbe4c Handle copy 2017-05-19 23:53:20 -07:00
Daniel Imms 2f6d05f99d Handle scrolling of selection 2017-05-19 23:32:26 -07:00
Daniel Imms ad3ae67e70 Get selection partially rendering 2017-05-19 23:29:51 -07:00
Daniel Imms b594407cd5 Tell renderer to draw selection 2017-05-19 20:06:05 -07:00
Daniel Imms 207c4cf961 More work on SelectionManager 2017-05-19 19:57:52 -07:00
Daniel Imms fe11837a61 Create new entries when shifting 2017-05-19 19:11:28 -07:00
Daniel Imms fee912023f Merge remote-tracking branch 'ups/master' into 207_selection_manager 2017-05-19 18:38:24 -07:00
Daniel Imms a95ac990ff Merge pull request #450 from Tyriar/449_keep_span_pool
Keep row spans in an object pool to reduce garbage collection by reusing DOM nodes
2017-05-16 10:12:27 -07:00
Daniel Imms aad8439542 Fix lint errors 2017-05-16 10:07:55 -07:00
Daniel Imms 1a777edffe Add missing return type 2017-05-16 10:04:12 -07:00
Daniel Imms 537018f71f DomElementObjectPool jsdoc 2017-05-16 10:03:56 -07:00
Daniel Imms 67ca3a9334 More string interpolation 2017-05-16 09:57:50 -07:00
Daniel Imms 5fb30979c1 Add jsdoc back, was lost in merge 2017-05-14 14:28:06 -07:00
Daniel Imms cf863d4849 Use string interpolation 2017-05-14 14:26:45 -07:00
Daniel Imms 47814847f9 Remove var usages 2017-05-14 12:47:17 -07:00
Daniel Imms 9ec8974545 More clean up 2017-05-14 12:40:45 -07:00
Daniel Imms c887c6e0f6 Clean up 2017-05-14 12:27:06 -07:00
Daniel Imms 6c8949bba1 Only wrap single width unicode chars 2017-05-14 12:20:01 -07:00
Daniel Imms 508d3ab68c Merge remote-tracking branch 'ups/master' into 449_keep_span_pool 2017-05-14 12:09:47 -07:00
Paris Kasidiaris 6dad013783 Merge pull request #659 from jdanyow/paste
Normalize line endings on paste
2017-05-11 12:28:38 +03:00
Jeremy Danyow 0532e5cbf0 Normalize line endings on paste 2017-05-09 15:21:56 -07:00
Daniel Imms abcdf3e252 Merge branch 'master' into 449_keep_span_pool 2017-05-08 11:35:42 -07:00
Daniel Imms 3baa6b9237 Wrap all characters temporarily in spans to force fixed with
Fixes #467
2017-05-07 16:08:01 -07:00
Daniel Imms 23e6a475ee Merge remote-tracking branch 'origin/master' into 449_keep_span_pool 2017-05-07 15:49:40 -07:00
Daniel Imms 65256c8751 Support an entry ID in CircularList
This is needed to keep track of which row is which outside of the class
2017-05-05 20:24:25 -07:00
Daniel Imms b36d8780fd Clean up 2017-05-05 17:40:23 -07:00
Daniel Imms 70fda99415 Basic fetching of coordinates 2017-05-05 17:36:15 -07:00
Paris Kasidiaris 2221f70ff0 Bump version to 2.6.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-05-04 15:38:52 +00:00
Daniel Imms daad8d9c04 Merge pull request #652 from Tyriar/651_fix_travis_fail
Force failing mocha tests to return a exit code of 1
2017-05-04 08:35:37 -07:00
Daniel Imms 03d26e39c7 Force failing mocha tests to return a exit code of 1
Fixes #651
2017-05-03 10:06:53 -07:00
Paris Kasidiaris 64d3c089a2 Merge pull request #647 from Martin1994/orion_use
Add "Eclipse Orion" to "Real-World Uses".
2017-05-02 00:37:38 +03:00
Martin Chloride 938c011e63 Added "Eclipse Orion" to "Real-World Uses". 2017-05-01 17:12:07 -04:00
Paris Kasidiaris 66f6b88333 Merge pull request #645 from sourcelair/issue-#640-do-not-focus-on-open
Add `focus` argument in `Terminal.prototype.open`
2017-05-01 01:07:00 +03:00
Paris Kasidiaris 35f746c925 Add focus argument in Terminal.prototype.open
Fix #640
2017-04-26 10:37:07 +00:00
Paris Kasidiaris 944da28089 Merge pull request #643 from jsmecham/patch-1
Add "Terminal for Atom" to Real-World Uses
2017-04-24 17:43:20 +03:00
Justin Mecham 8f3e2b645a Added "Terminal for Atom" to Real-World Uses. 2017-04-23 14:36:01 -04:00
Daniel Imms 82b2a5305f Merge pull request #639 from Tyriar/638_retain_cursor_blink_interface
Keep cursor blink interval after a reset
2017-04-21 17:59:51 -07:00
Daniel Imms 6a2852ee26 Merge branch 'master' into 638_retain_cursor_blink_interface 2017-04-21 17:53:49 -07:00
Daniel Imms 526d6c4da9 Merge pull request #641 from Tyriar/support_demo_cursorStyle
Support cursorStyle selection in demo
2017-04-20 10:33:30 -07:00
Daniel Imms 46d62f4972 Support cursorStyle selection in demo 2017-04-19 18:16:48 -07:00
Daniel Imms 5cafcdb695 Keep cursor blink interval after a reset
Fixes #638
2017-04-19 11:22:18 -07:00
Paris Kasidiaris 2674258ead Merge pull request #635 from rstudio/master
Update README with RStudio under real-world uses.
2017-04-15 01:15:27 +03:00
Gary Ritchie b38be3a1d0 Update README with RStudio under real-world uses. 2017-04-14 10:50:15 -07:00
Daniel Imms 18a9f05626 Merge pull request #628 from saulcosta18/patch-1
Added Codevolve.com to README
2017-04-06 13:01:16 -07:00
Saul Costa bf34980c43 Added Codevolve.com to README
We use xterm.js for our courses at Codevolve.
2017-04-06 12:50:41 -07:00
Paris Kasidiaris d2f8f9dc1a Bump version to 2.5.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-04-05 18:48:57 +00:00
Daniel Imms df13469b6e Merge pull request #627 from Tyriar/591_element_on_validation
Give API access to valid link elements
2017-04-05 11:34:34 -07:00
Daniel Imms 584ec681af Merge remote-tracking branch 'ups/master' into 591_element_on_validation 2017-04-04 09:50:04 -07:00
Daniel Imms 0c5b5cca4a Merge pull request #623 from Tyriar/612_multiple_links_in_row
Support multiple link matches in a single row
2017-04-04 09:41:19 -07:00
Daniel Imms 64663bf4a8 Fix tests 2017-04-04 09:35:52 -07:00
Daniel Imms fe8334a873 Merge remote-tracking branch 'ups/master' into 612_multiple_links_in_row 2017-04-04 09:33:53 -07:00
Daniel Imms c74fd40e2a Merge pull request #619 from Tyriar/618_register_link_before_dom
Allow links to be registered before terminal is attached to DOM
2017-04-04 09:13:09 -07:00
Daniel Imms 0f8ff67451 Merge branch 'master' into 618_register_link_before_dom 2017-04-04 09:02:37 -07:00
Daniel Imms 8b0032c210 Merge branch 'master' into 612_multiple_links_in_row 2017-04-04 09:02:35 -07:00
Daniel Imms d9254ef2d9 Merge pull request #626 from Tyriar/625_cursor_blink_perf
Use setInterval over animation for cursor blink
2017-04-04 09:02:27 -07:00
Daniel Imms 66be05a962 Merge branch 'master' into 612_multiple_links_in_row 2017-04-04 09:00:47 -07:00
Daniel Imms 0e85a09dd3 Merge branch 'master' into 618_register_link_before_dom 2017-04-04 08:58:43 -07:00
Daniel Imms 0c4ba9fd67 Clear cursor blink interval on blur 2017-04-03 15:09:38 -07:00
Daniel Imms aca81c764a Use setInterval over animation for cursor blink
Fixes #625
2017-04-03 14:49:21 -07:00
Daniel Imms 6b3ddb97d1 Merge pull request #624 from InDieTasten/patch-1
Fix typos in comment
2017-03-31 13:27:29 -07:00
InDieTasten 26ccf2a316 Fix typos in comment 2017-03-31 22:16:11 +02:00
Daniel Imms 361894194a Merge branch 'master' into 612_multiple_links_in_row 2017-03-31 03:06:23 -07:00
Daniel Imms 8c2db8ddb8 More tests 2017-03-31 02:10:07 -07:00
Daniel Imms 5546baa927 Fix edge case 2017-03-31 01:54:21 -07:00
Daniel Imms d814009721 Add tests 2017-03-31 01:48:51 -07:00
Daniel Imms 08fd050c3f Support multiple link matches in a single row
Fixes #612
2017-03-31 01:48:35 -07:00
Daniel Imms 0b096d7d55 Merge pull request #621 from coderaiser/patch-1
Added Cloud Commander to real world uses
2017-03-30 11:31:11 -07:00
coderaiser 44cfb2dc87 Added Cloud Commander to real world uses 2017-03-30 14:26:56 +03:00
Daniel Imms b0624cadb7 Allow links to be registered before terminal is attached to DOM
Fixes #618
2017-03-24 11:04:04 -07:00
Paris Kasidiaris 045f361b0d Merge pull request #616 from LucianBuzzo/crop-lines
Prevent line data from being discarded on resize.
2017-03-20 09:52:22 +00:00
Lucian Buzzo cb77b9dc1a Prevent line data from being discarded on resize. 2017-03-19 08:26:37 +00:00
Paris Kasidiaris 0723dac596 Merge pull request #615 from andfoy/patch-1
Added Spyder IDE Terminal as a xterm case-use application
2017-03-19 00:26:41 +00:00
Edgar Andrés Margffoy Tuay 365f9b4c1b Added Spyder IDE as a xterm case-use application 2017-03-18 18:05:05 -05:00
Daniel Imms 2cf90d90f7 Merge pull request #610 from Tyriar/non_draggable_links
Ensure terminal links are not draggable
2017-03-17 09:22:05 -07:00
Daniel Imms da14f64d1b Merge branch 'master' into non_draggable_links 2017-03-17 02:27:07 -07:00
Paris Kasidiaris ef1b1c8ffc Merge pull request #608 from sourcelair/add-logo
Add xterm.js logo
2017-03-17 08:18:08 +00:00
Daniel Imms adde8ab2c3 Ensure terminal links are not draggable 2017-03-16 12:07:25 -07:00
Paris Kasidiaris 22cc8079dc Add xterm.js logo 2017-03-16 08:54:07 +00:00
Daniel Imms 6d25706300 Merge pull request #606 from Tyriar/vscode22640_tilde_path
Allow ~ char in linkify path and query fragments
2017-03-15 18:01:47 -07:00
Paris Kasidiaris 81419c980b Merge pull request #603 from sourcelair/fix-blurred-terminal-cursor
Fix cursor style on blurred terminals
2017-03-15 22:09:18 +00:00
Paris Kasidiaris 097e21f641 Merge pull request #604 from sourcelair/rebuild-on-css
Rebuild xterm.js even when CSS changes
2017-03-15 22:09:03 +00:00
Daniel Imms b68180b97b Allow ~ char in linkify path and query fragments 2017-03-15 13:55:57 -07:00
Daniel Imms d9059d18c0 Merge pull request #600 from sourcelair/Tyriar-patch-1
Polish development and contribution section
2017-03-15 10:53:03 -07:00
Paris Kasidiaris 5225a6e27a Rebuild xterm.js even when CSS changes
Otherwise you had to run `npm build` manually every time you changed a CSS file to see the updated result in the demo.
2017-03-15 18:36:06 +02:00
Paris Kasidiaris 9055bace96 Fix cursor style on blurred terminals 2017-03-15 18:28:17 +02:00
Paris Kasidiaris eb25e88754 Merge pull request #598 from mikesir87/fix-fit-calculation
Fixed characterHeight calculation, which could cause overflowing
2017-03-15 14:34:13 +00:00
Michael Irwin 0cb8ecc6f7 Use boundingClientRect instead of style to get actual height
PR #598
2017-03-15 09:06:56 -04:00
Daniel Imms 8cf76cc6e9 Polish development and contribution section 2017-03-14 15:15:36 -07:00
Michael Irwin 3c3a646d42 Fixed characterHeight calculation, which could cause overflowing
The previous calculation was simply using the height of the letter
W, but wasn't taking into account that each row has a lineHeight,
which might be greater. If the lineHeight is .4px off, after many
rows, it will cause the proposedGeometry to have an extra row,
potentially causing hidden rows (based on layout).
2017-03-14 17:02:53 -04:00
Paris Kasidiaris ff29fd69a3 Merge pull request #572 from AndrienkoAleksandr/gulpImprove
Use tsconfig.json data in gulpfile.js instead of hardcoded values.
2017-03-11 13:21:47 +00:00
Daniel Imms 91994a399c Merge pull request #582 from tmyt/propose/row-height
Support checking height in checkBoldBroken
2017-03-10 12:53:12 -08:00
Daniel Imms bef7137c92 Fix bug 2017-03-09 10:47:09 -08:00
Daniel Imms 11f62bab34 Expose setHypertextValidationCallback 2017-03-09 10:39:08 -08:00
Daniel Imms b66776a047 Include element in validationCallback
Fixes #591
2017-03-09 10:23:59 -08:00
Daniel Imms 2c151f444c Merge pull request #589 from warpdesign/patch-1
FIXED: typo
2017-03-08 15:52:55 -08:00
Nicolas Ramz d8f8b060cb FIXED: typo
`<div id="terminal"></div>` wasn't used, instead, a new one was created and used since getElementById('#terminal') returned null.
2017-03-09 00:47:29 +01:00
Daniel Imms ea9376019c Merge pull request #586 from AndrienkoAleksandr/cleanUpTsConfig
Clean up tsconfig.json.
2017-03-08 12:02:09 -08:00
Daniel Imms 008d1df106 Merge remote-tracking branch 'up/master' into cleanUpTsConfig 2017-03-08 11:47:56 -08:00
Daniel Imms ec91a6c7f4 Merge pull request #588 from Tyriar/587_csi_3_J
Implement CSI 3 J - Erase saved lines
2017-03-08 11:08:46 -08:00
yutaka 4b2ae6a79f Use rendered height for detect broken bold font. 2017-03-08 11:30:23 +00:00
yutaka 2ec756fd6b Use offsetWidth instead of scrollWidth
scrollWidth does not work on Chrome with `display: inline` element.
2017-03-08 05:11:26 +00:00
yutaka f0d5b40121 Detect bold font was broken correctly. 2017-03-08 04:54:18 +00:00
yutaka 31a0996b2f Revert "Set charMeasure.height values to each row height"
This reverts commit 43cb4f471f.
2017-03-08 00:27:22 +00:00
Daniel Imms 0bbd8f0a16 Implement CSI 3 J - Erase saved lines
Fixes #587
2017-03-07 14:01:42 -08:00
Paris Kasidiaris b1619e42fd Bump version to 2.4.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-03-07 09:38:48 +00:00
Daniel Imms 4cfec2fc09 Merge pull request #579 from Tyriar/578_ctrl_click_links
Require ctrl for link clicks
2017-03-06 07:21:58 -08:00
Aleksandr Andrienko 3d721a95bb Clean up tsconfig.json.
'include' field in the tsconfig.json defines source scope (top level of this scope is "src" folder), so we do not need 'exclude' folders in the root of the project, because this folders had already excluded by 'include' scope.

Signed-off-by: Aleksandr Andrienko <aandrienko@codenvy.com>
2017-03-06 13:17:01 +02:00
Aleksandr Andrienko dc3a136704 Use tsconfig.json data in gulpfile.js instead of hardcoded values.
Use tsconfig.json data in gulpfile.js instead of hardcoded values. Little code clean up

Signed-off-by: Aleksandr Andrienko <aandrienko@codenvy.com>
2017-03-06 12:25:22 +02:00
Daniel Imms 235ae2fbb9 Fix http link handler logic 2017-03-04 15:02:08 -08:00
Daniel Imms 1f518b0b45 Allow http link handler to fallback to standard handler 2017-03-04 14:44:11 -08:00
Daniel Imms ab34d53d6b Remove unused import 2017-03-04 14:09:13 -08:00
Daniel Imms aafb5333b9 Give event to handler 2017-03-04 14:08:05 -08:00
Daniel Imms 5e2791f981 Revert "Add fail handler"
This reverts commit 1ecc5674ac.
2017-03-04 14:01:40 -08:00
Daniel Imms 1ecc5674ac Add fail handler 2017-03-04 14:01:34 -08:00
Daniel Imms c9ad3a595a Merge pull request #584 from coderaiser/patch-1
Fix demo: wrong pid on windows #567
2017-03-03 14:38:55 -08:00
coderaiser 43558fa1dc Fix demo: wrong pid on windows #567 2017-03-03 23:40:36 +02:00
Daniel Imms 15a867d2b7 Support cmd+click on mac 2017-03-03 12:37:34 -08:00
yutaka 43cb4f471f Set charMeasure.height values to each row height 2017-03-03 07:57:05 +00:00
Daniel Imms 012051c1db Fix matchIndex on http 2017-03-02 17:35:10 -08:00
Daniel Imms b5483898da Merge remote-tracking branch 'upstream/master' into 578_ctrl_click_links 2017-03-02 17:34:30 -08:00
Daniel Imms c7b4c2beaf Require ctrl for link clicks
Fixes #578
2017-03-02 17:33:58 -08:00
Daniel Imms 8693d067e3 Merge pull request #577 from Tyriar/549_link_priority
Implement link matcher priorities
2017-03-02 15:24:09 -08:00
Daniel Imms 78d5fc9506 Fix bug with priority 2017-03-02 12:05:34 -08:00
Daniel Imms 7ac4f1a9be Implement link matcher priorities
Fixes #549
2017-03-02 11:25:27 -08:00
Daniel Imms 670fb01fbf Merge pull request #566 from Tyriar/linkify_issues
Linkify issues
2017-03-02 09:02:24 -08:00
Daniel Imms ec9608d54b Merge pull request #574 from Tyriar/link_validation_fixes
Link validation fixes
2017-03-02 00:42:01 -08:00
Daniel Imms dcffaf1c8c Ensure linkElement exists before firing validation callback 2017-03-01 09:38:59 -08:00
Daniel Imms efac5293c0 Use text cursor for invalid links
Part of #570
2017-03-01 09:37:30 -08:00
Daniel Imms c7563346a2 Merge pull request #571 from Tyriar/570_link_validation
Implement link validation
2017-03-01 09:12:53 -08:00
Daniel Imms 69906532b1 Remove test harness 2017-02-27 15:44:10 -08:00
Daniel Imms 9404fae725 Merge remote-tracking branch 'upstream/master' into linkify_issues 2017-02-27 12:59:59 -08:00
Daniel Imms 3542f15575 Don't expose CharMeasure/Linkifier for tests 2017-02-27 12:57:18 -08:00
Daniel Imms 15d79143e2 Get rid of exposed test method (phantom is gone) 2017-02-27 12:56:54 -08:00
Daniel Imms f6dafee748 Remove dead code 2017-02-27 12:52:54 -08:00
Daniel Imms 356d86ae74 Merge remote-tracking branch 'upstream/master' into 570_link_validation 2017-02-27 12:47:53 -08:00
Daniel Imms e673c6ec97 Upgrade to TypeScript 2.2 2017-02-27 11:54:55 -08:00
Daniel Imms 3e5fcf7004 Merge pull request #569 from wavebeem/issue/562
Fixes #562 adds Windows demo setup help
2017-02-27 11:52:47 -08:00
Daniel Imms 96d2a2bc68 Pin TypeScript at v2.1 to fix PhantomJS error 2017-02-27 11:47:57 -08:00
Daniel Imms b401992178 Add jsdom types 2017-02-27 11:22:59 -08:00
Daniel Imms 4c34351383 Remove PhantomJS completely 2017-02-27 11:22:50 -08:00
Daniel Imms 202b54afbb Convert CharMeasure phantom test to jsdom 2017-02-27 11:18:00 -08:00
Daniel Imms 26ebc3d944 Use jsdom instead of phantomjs for linkifier test 2017-02-27 11:00:12 -08:00
Daniel Imms d0f36e9939 Pin typescript 2.1 to prevent exports error 2017-02-27 10:02:12 -08:00
Daniel Imms 4c99c032ca Add tests 2017-02-27 09:47:36 -08:00
Daniel Imms 6198556e50 Implement link validation
Fixes #570
2017-02-27 08:35:59 -08:00
Brian Mock 514eba0e4e Fixes #562 adds Windows demo setup help 2017-02-25 12:16:18 -08:00
Daniel Imms 7279ee0f5f Support % in URLs 2017-02-24 08:56:42 -08:00
Daniel Imms ccadf3fcca Support percent and numbers in query/hash fragments 2017-02-23 10:02:18 -08:00
Daniel Imms cb80ce0d2c Link . and - in hash fragments and query strings 2017-02-23 09:55:05 -08:00
Daniel Imms 9aae43961f Support linking hash fragments 2017-02-23 09:53:20 -08:00
Daniel Imms b810687163 Support linking = in query strings 2017-02-23 09:42:59 -08:00
Daniel Imms 28d4ec779d Fix localhost links 2017-02-23 09:37:35 -08:00
Paris Kasidiaris cf4da6c748 Merge pull request #565 from sourcelair/issue-#531-coveralls
Fix #531: Submit code coverage to coveralls.io
2017-02-22 19:15:04 +02:00
Paris Kasidiaris e569b5669a Add coveralls badge 2017-02-22 16:22:50 +02:00
Paris Kasidiaris 695a9523fe Fix #531: Submit code coverage to coveralls.io 2017-02-22 16:17:22 +02:00
Daniel Imms 01b8e28a7d Merge pull request #556 from Tyriar/555_fix_mouse_coords
Use CharMeasure to determine mouse coordinates
2017-02-20 03:17:50 -08:00
Daniel Imms e221030a2b Merge pull request #559 from Tyriar/558_linkify_bad_trim
Remove bad trim optimization from Linkifier
2017-02-20 03:17:42 -08:00
Daniel Imms 88dc697392 Merge pull request #557 from Tyriar/CharMeasure_aria_hidden
Hide CharMeasure element from assistive technologies
2017-02-20 03:17:34 -08:00
Daniel Imms 99a270216c Remove bad trim optimization from Linkifier
This prevents whitespace from going missing after links are applied.

Fixes #558
2017-02-19 15:17:03 -08:00
Daniel Imms d06c920a2d Hide CharMeasure element from assistive technologies 2017-02-19 13:20:36 -08:00
Daniel Imms 5452986ce3 Use CharMeasure to determine mouse coordinates
Fixes #555
2017-02-19 12:34:36 -08:00
Daniel Imms ae69841174 Merge pull request #538 from Tyriar/455_linkify
Implement web links and custom link matcher registration
2017-02-17 11:40:59 -08:00
Daniel Imms e6fc80c13b Fix link matcher iteration direction 2017-02-17 10:22:55 -08:00
Daniel Imms a10ee8f58c Revert priority change, http handler needs to be top 2017-02-17 10:18:46 -08:00
Daniel Imms 3f69da2455 Support query string in linkifier 2017-02-17 10:06:35 -08:00
Daniel Imms a14a0834e9 Merge remote-tracking branch 'upstream/master' into 455_linkify 2017-02-17 10:05:22 -08:00
Daniel Imms c4f431849b Add a null check on linkifyRow 2017-02-16 22:41:08 -08:00
Daniel Imms ade3389cea Merge pull request #535 from Tyriar/534_phantomjs
Add PhantomJS test support and CharMeasure tests
2017-02-15 13:29:50 -08:00
Paris Kasidiaris 6873ba0909 Merge pull request #541 from AndrienkoAleksandr/FixSoftReset
Fix 'Autowrap Mode' after 'Soft Terminal reset' command.
2017-02-15 11:24:42 +02:00
Daniel Imms c3f31b5f65 Apply link matchers in reverse
This enables consumers to have full control.
2017-02-14 20:06:15 -08:00
Daniel Imms 3b62aa444b Improve jsdoc 2017-02-14 19:45:22 -08:00
Daniel Imms fb2b10b565 Merge pull request #545 from Martin1994/blink_cursor_color
Let blink animation support style inheritance
2017-02-14 19:23:22 -08:00
Martin Wang 345553012f Let blink animation support style inheritance
With this change, blinking cursor will use the same font and background
color that set before.
2017-02-14 12:35:39 -05:00
Daniel Imms fddc13d98e Merge pull request #544 from LucianBuzzo/master
Make fit plugin guard against non-existent parent element.
2017-02-13 13:26:14 -08:00
Lucian Buzzo aeb9f798c9 Make fit plugin guard against non-existent parent element. 2017-02-13 21:01:28 +00:00
Paris Kasidiaris 8fd44266a2 Bump version to 2.3.2
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2017-02-13 18:05:47 +00:00
Paris Kasidiaris ef4155fbf6 Merge pull request #542 from Tyriar/539_fix_initial_viewport_sync
Perform initial viewport sync after CharMeasure is ready
2017-02-13 20:05:03 +02:00
Daniel Imms 92f27d3144 Fix tests 2017-02-13 09:48:22 -08:00
Daniel Imms 5183332f6e Next link matcher ID must be non-static 2017-02-10 12:29:36 -08:00
Daniel Imms ee1de0980f Perform initial viewport sync after CharMeasure is ready
Fixes #539
2017-02-10 09:55:35 -08:00
Aleksandr Andrienko c53183d94b Fix default 'wraparoundMode' value for 'Soft reset'() XTERM terminal. 2017-02-10 16:52:19 +02:00
Daniel Imms 1ee774d01d Remove linkify test 2017-02-09 22:03:31 -08:00
Daniel Imms 1c030f57ef Force a refresh after register/deregister link matcher 2017-02-09 21:12:28 -08:00
Daniel Imms c8bb321654 Get custom link matcher working 2017-02-09 21:08:50 -08:00
Daniel Imms a1d71c9198 Properly queue linkify after refresh 2017-02-09 20:35:06 -08:00
Daniel Imms 55cb43d70b Add license 2017-02-09 20:12:35 -08:00
Daniel Imms 551159e0c3 Remove linkify addon 2017-02-09 20:10:01 -08:00
Daniel Imms 3bf31aa478 Polish 2017-02-09 20:09:47 -08:00
Daniel Imms 1316682343 Merge pull request #532 from AndrienkoAleksandr/cleanUpDependency
Remove excess library.
2017-02-09 20:01:46 -08:00
Daniel Imms 7167b06bf8 Add custom link handlers, use generic way for http handler 2017-02-08 20:44:22 -08:00
Daniel Imms 34582f9f65 Remove logs 2017-02-08 20:21:18 -08:00
Daniel Imms 8dc9ec5733 Only show underline on hover 2017-02-08 20:20:18 -08:00
Daniel Imms e8adf8ad2b Use gulp-mocha and attach run both test type in npm test 2017-02-08 20:06:14 -08:00
Daniel Imms 1306431e7c Add PhantomJS test support and CharMeasure tests
Fixes #534
2017-02-08 19:53:32 -08:00
Daniel Imms f7bc0fba4d Remove unnecessary variables 2017-02-08 18:57:45 -08:00
Daniel Imms 0f3ee21d9d Enable custom http link handlers 2017-02-08 14:13:30 -08:00
Daniel Imms a489037ec4 Improve node insertion, support custom link handlers 2017-02-08 13:50:36 -08:00
Aleksandr Andrienko c48388af14 Remove excess library.
Tsify lib used for browserify typescript sources. But folder /lib consist of js files has already compiled from TypeScript sources (by gulp task tsc). So Tsify library does nothing in this case.

Signed-off-by: Aleksandr Andrienko <aandrienko@codenvy.com>
2017-02-08 23:15:54 +02:00
Daniel Imms 2207d356f2 Initial linkify implementation
Part of 455
2017-02-08 11:11:52 -08:00
Daniel Imms 6b5ced0078 Merge remote-tracking branch 'upstream/master' into 449_keep_span_pool 2017-01-08 17:09:23 -08:00
Daniel Imms c7e8498166 Clear row and return spans before generating row 2017-01-04 09:44:58 -08:00
Daniel Imms 2b4c019f13 Use object pool in refresh 2017-01-04 09:32:23 -08:00
Daniel Imms 335850380e Implement DomElementObjectPool with tests 2017-01-04 08:50:19 -08:00
61 changed files with 18859 additions and 2034 deletions
+1
View File
@@ -0,0 +1 @@
* text=auto
+4
View File
@@ -13,3 +13,7 @@ npm-debug.log
.env
build/
.vscode/
.DS_Store
# Directories needed for code coverage
/coverage/
+10 -8
View File
@@ -1,18 +1,20 @@
language: node_js
os:
- linux
- osx
node_js:
- 6
before_install:
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
- npm install -g npm@5.1.0
env:
global:
- CXX=g++-4.8
matrix:
- NPM_COMMAND=lint
- NPM_COMMAND=test
addons:
apt:
sources:
- ubuntu-toolchain-r-test
packages:
- g++-4.8
notifications:
email: false
script: npm run $NPM_COMMAND
after_success: npm run coveralls
+9
View File
@@ -0,0 +1,9 @@
{
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/dist": true,
"**/build": true,
"**/lib": true
}
}
+33
View File
@@ -1,10 +1,16 @@
List of xterm.js contributors. Updated before every release.
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
Austin Robertson <austinrobertson@gmail.com>
ayapi <colors.aya@gmail.com>
Ben Hall <ben@benhall.me.uk>
@@ -12,30 +18,57 @@ Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brian Mock <brian@mockbrian.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
Dan Kaplun <dbkaplun@twitch.tv>
Darin Morrison <freebroccolo@users.noreply.github.com>
Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
Ian Lewis <ianlewis@google.com>
imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Lucian Buzzo <lucian.buzzo@gmail.com>
Maël Nison <nison.mael@gmail.com>
Martin Chloride <i@martincl2.me>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
Nicolas Ramz <nicolas.ramz@gmail.com>
Oleksandr Andriienko <oandriie@redhat.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
runarberg <runar@greenqloud.com>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
t-amqi <umberos@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
thomas <thomas@kreativkonzentrat.de>
Thomas Zilz <t.zilz@mofux.org>
Tine Jozelj <tine.jozelj@outlook.com>
Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
+38 -30
View File
@@ -1,6 +1,6 @@
# xterm.js
# [![xterm.js logo](logo.png)](https://xtermjs.org)
[![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
[![xterm.js build status](https://api.travis-ci.org/sourcelair/xterm.js.svg)](https://travis-ci.org/sourcelair/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
@@ -30,6 +30,19 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
@@ -48,14 +61,29 @@ Xterm.js works seamlessly in Electron apps and may even work on earlier versions
## Demo
To launch the demo simply run:
### Linux or macOS
Run the following commands:
```
npm install
npm start
$ npm install
$ npm start
```
Then open http://0.0.0.0:3000 in a web browser (use http://127.0.0.1:3000 if running under Windows).
Then open http://0.0.0.0:3000 in a web browser.
### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
Note: Do not use ConEmu, as it seems to break the demo for some reason.
```
> npm install
> npm start
```
Then open http://127.0.0.1:3000 in a web browser.
## Getting Started
@@ -71,13 +99,13 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<div id="terminal"></div>
<script>
var term = new Terminal();
term.open(document.getElementById('#terminal'));
term.open(document.getElementById('terminal'));
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
</script>
</body>
</html>
```
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
## Addons
@@ -106,32 +134,12 @@ The existing releases are available at this GitHub repo's [Releases](https://git
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) before.
The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript and a browser (if you would like to run the demo you will need Node.js to get all features).
It is recommended though to use a development tool that uses xterm.js internally, to develop for xterm.js. [Eating our own dogfood](https://en.wikipedia.org/wiki/Eating_your_own_dog_food) has been proved extremely beneficial for this project. Known tools that use xterm.js internally are:
#### [SourceLair](https://www.sourcelair.com)
Visit https://lair.io/sourcelair/xterm and follow the instructions. All development will happen in your browser.
#### [Visual Studio Code](http://code.visualstudio.com/)
[Download Visual Studio Code](http://code.visualstudio.com/Download), clone xterm.js and you are all set.
#### [Eclipse Che](http://www.eclipse.org/che)
You can start Eclipse Che with `docker run eclipse/che start`.
#### [Codenvy](http://www.codenvy.io)
You can create a trial account or install an enterprise version with `docker run codenvy/cli start`.
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2014-2016, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "xterm.js",
"version": "2.3.1",
"version": "2.9.0",
"ignore": ["demo", "test", ".gitignore"],
"main": [
"dist/xterm.js",
+1 -1
View File
@@ -69,7 +69,7 @@ app.ws('/terminals/:pid', function (ws, req) {
term.write(msg);
});
ws.on('close', function () {
process.kill(term.pid);
term.kill();
console.log('Closed terminal ' + term.pid);
// Clean things up
delete terminals[term.pid];
+19
View File
@@ -5,20 +5,39 @@
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
<script src="/build/xterm.js" ></script>
<script src="/build/addons/attach/attach.js" ></script>
<script src="/build/addons/fit/fit.js" ></script>
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
<script src="/build/addons/search/search.js" ></script>
</head>
<body>
<h1>xterm.js: xterm, in the browser</h1>
<div id="terminal-container"></div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label>
cursorStyle
<select id="option-cursor-style">
<option value="block">block</option>
<option value="underline">underline</option>
<option value="bar">bar</option>
</select>
</label>
</p>
<p>
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
</p>
+21
View File
@@ -7,8 +7,13 @@ var term,
charHeight;
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth')
},
@@ -29,9 +34,25 @@ function setTerminalSize () {
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
optionElements.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
+7 -2
View File
@@ -32,6 +32,9 @@
var exports = {};
exports.proposeGeometry = function (term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
@@ -53,7 +56,7 @@
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = parseInt(subjectRow.offsetHeight);
characterHeight = subjectRow.getBoundingClientRect().height;
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
@@ -66,7 +69,9 @@
exports.fit = function (term) {
var geometry = exports.proposeGeometry(term);
term.resize(geometry.cols, geometry.rows);
if (geometry) {
term.resize(geometry.cols, geometry.rows);
}
};
Xterm.prototype.proposeGeometry = function () {
+56 -37
View File
@@ -41,6 +41,9 @@
font-family: courier-new, courier, monospace;
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.terminal.focus,
@@ -71,63 +74,55 @@
resize: none;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
background-color: #fff;
color: #000;
.terminal a {
color: inherit;
text-decoration: none;
}
.terminal a:hover {
cursor: pointer;
text-decoration: underline;
}
.terminal a.xterm-invalid-link:hover {
cursor: text;
text-decoration: none;
}
.terminal .terminal-cursor {
position: relative;
}
.terminal:not(.focus) .terminal-cursor {
outline: 1px solid #fff;
outline-offset: -1px;
background-color: transparent;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor {
animation: xterm-cursor-blink 1.2s infinite step-end;
.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor {
background-color: #fff;
color: #000;
}
@keyframes xterm-cursor-blink {
0% {
background-color: #fff;
color: #000;
}
50% {
background-color: transparent;
color: #FFF;
}
}
.terminal.xterm-cursor-style-bar .terminal-cursor,
.terminal.xterm-cursor-style-underline .terminal-cursor {
position: relative;
}
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
content: "";
display: block;
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before,
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
content: '';
position: absolute;
background-color: #fff;
}
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before {
top: 0;
bottom: 0;
left: 0;
bottom: 0;
width: 1px;
}
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
bottom: 0;
left: 0;
right: 0;
height: 1px;
}
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
animation: xterm-cursor-non-bar-blink 1.2s infinite step-end;
}
@keyframes xterm-cursor-non-bar-blink {
0% { background-color: #fff; }
50% { background-color: transparent; }
}
.terminal .composition-view {
background: #000;
@@ -148,7 +143,8 @@
overflow-y: scroll;
}
.terminal .xterm-wide-char {
.terminal .xterm-wide-char,
.terminal .xterm-normal-char {
display: inline-block;
}
@@ -174,6 +170,25 @@
left: -9999em;
}
.terminal.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.terminal .xterm-selection {
position: absolute;
top: 0;
left: 0;
z-index: 1;
opacity: 0.3;
pointer-events: none;
}
.terminal .xterm-selection div {
position: absolute;
background-color: #fff;
}
/*
* Determine default colors for xterm.js
*/
@@ -189,6 +204,10 @@
text-decoration: blink;
}
.terminal .xterm-blink.xterm-underline {
text-decoration: blink underline;
}
.terminal .xterm-hidden {
visibility: hidden;
}
+1832 -584
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
+107 -24
View File
@@ -1,39 +1,57 @@
/**
* @license MIT
*/
const browserify = require('browserify');
const buffer = require('vinyl-buffer');
const coveralls = require('gulp-coveralls');
const fs = require('fs-extra');
const gulp = require('gulp');
const istanbul = require('gulp-istanbul');
const merge = require('merge-stream');
const mocha = require('gulp-mocha');
const sorcery = require('sorcery');
const source = require('vinyl-source-stream');
const sourcemaps = require('gulp-sourcemaps');
const ts = require('gulp-typescript');
const tsify = require('tsify');
const util = require('gulp-util');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
let tsProjectSearchAddon = ts.createProject('./src/addons/search/tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
// Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute.
if (outDir.indexOf(__dirname) !== 0) {
outDir = `${__dirname}/${outDir}`;
}
/**
* Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript
* file compiled.
*/
gulp.task('tsc', function () {
// Remove the lib/ directory to prevent confusion if files were deleted in src/
fs.emptyDirSync('lib');
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/
fs.emptyDirSync(`${outDir}`);
// Build all TypeScript files (including tests) to lib/, based on the configuration defined in
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in
// `tsconfig.json`.
let tsProject = ts.createProject('tsconfig.json');
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest('lib'));
let tsc = tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir));
// Copy all addons from src/ to lib/
let copyAddons = gulp.src('src/addons/**/*').pipe(gulp.dest('lib/addons'));
fs.emptyDirSync(`${outDir}/addons/search`);
let tsResultSearchAddon = tsProjectSearchAddon.src().pipe(sourcemaps.init()).pipe(tsProjectSearchAddon());
let tscSearchAddon = tsResultSearchAddon.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(`${outDir}/addons/search`));
// Copy stylesheets from src/ to lib/
let copyStylesheets = gulp.src('src/**/*.css').pipe(gulp.dest('lib'));
// Copy all addons from ${srcDir}/ to ${outDir}/
let copyAddons = gulp.src([`${srcDir}/addons/**/*`, `!${srcDir}/addons/search`, `!${srcDir}/addons/search/**`]).pipe(gulp.dest(`${outDir}/addons`));
return merge(tsc, copyAddons, copyStylesheets);
// Copy stylesheets from ${srcDir}/ to ${outDir}/
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
return merge(tsc, tscSearchAddon, copyAddons, copyStylesheets);
});
/**
@@ -47,13 +65,12 @@ gulp.task('browserify', ['tsc'], function() {
let browserifyOptions = {
basedir: buildDir,
debug: true,
entries: ['../lib/xterm.js'],
entries: [`${outDir}/xterm.js`],
standalone: 'Terminal',
cache: {},
packageCache: {}
};
let bundleStream = browserify(browserifyOptions)
.plugin(tsify)
.bundle()
.pipe(source('xterm.js'))
.pipe(buffer())
@@ -61,27 +78,93 @@ gulp.task('browserify', ['tsc'], function() {
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from lib/ to buildDir
let copyAddons = gulp.src('lib/addons/**/*').pipe(gulp.dest(`${buildDir}/addons`));
// Copy stylesheets from ${outDir}/ to ${buildDir}/
let copyStylesheets = gulp.src(`${outDir}/**/*.css`).pipe(gulp.dest(buildDir));
// Copy stylesheets from src/ to lib/
let copyStylesheets = gulp.src('lib/**/*.css').pipe(gulp.dest(buildDir));
return merge(bundleStream, copyAddons, copyStylesheets);
return merge(bundleStream, copyStylesheets);
});
gulp.task('browserify-addons', ['tsc'], function() {
let searchOptions = {
basedir: `${buildDir}/addons/search`,
debug: true,
entries: [`${outDir}/addons/search/search.js`],
cache: {},
packageCache: {}
};
let searchBundle = browserify(searchOptions)
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from outDir to buildDir
let copyAddons = gulp.src([
// Copy JS addons
`${outDir}/addons/**/*`,
// Exclude TS addons from copy as they are being built via browserify
`!${outDir}/addons/search`,
`!${outDir}/addons/search/**`
]).pipe(gulp.dest(`${buildDir}/addons`));
return merge(searchBundle, copyAddons);
});
gulp.task('instrument-test', function () {
return gulp.src([`${outDir}/**/*.js`])
// Covering files
.pipe(istanbul())
// Force `require` to return covered files
.pipe(istanbul.hookRequire());
});
gulp.task('mocha', ['instrument-test'], function () {
return gulp.src([`${outDir}/*test.js`, `${outDir}/**/*test.js`], {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
});
/**
* Run single test file by file name(without file extension). Example of the command:
* gulp mocha-test --test InputHandler.test
*/
gulp.task('mocha-test', ['instrument-test'], function () {
let testName = util.env.test;
util.log("Run test by Name: " + testName);
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
});
/**
* Use `sorcery` to resolve the source map chain and point back to the TypeScript files.
* (Without this task the source maps produced for the JavaScript bundle points into the
* compiled JavaScript files in lib/).
* compiled JavaScript files in ${outDir}/).
*/
gulp.task('sorcery', ['browserify'], function () {
var chain = sorcery.loadSync(`${buildDir}/xterm.js`);
var map = chain.apply();
let chain = sorcery.loadSync(`${buildDir}/xterm.js`);
chain.apply();
chain.writeSync();
});
gulp.task('build', ['sorcery']);
gulp.task('sorcery-addons', ['browserify-addons'], function () {
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
chain.apply();
chain.writeSync();
});
/**
* Submit coverage results to coveralls.io
*/
gulp.task('coveralls', function () {
gulp.src('coverage/**/lcov.info')
.pipe(coveralls());
});
gulp.task('build', ['sorcery', 'sorcery-addons']);
gulp.task('test', ['mocha']);
gulp.task('default', ['build']);
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

+12552
View File
File diff suppressed because it is too large Load Diff
+14 -10
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "2.3.1",
"version": "2.9.0",
"ignore": [
"demo",
"test",
@@ -36,40 +36,44 @@
],
"devDependencies": {
"@types/chai": "^3.4.34",
"@types/jsdom": "^11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "^6.0.41",
"browserify": "^13.1.0",
"browserify": "^13.3.0",
"chai": "3.5.0",
"docdash": "0.4.0",
"express": "4.13.4",
"express-ws": "2.0.0-rc.1",
"fs-extra": "^1.0.0",
"glob": "^7.0.5",
"gulp": "^3.9.1",
"gulp": "3.9.1",
"gulp-cli": "^1.2.2",
"gulp-coveralls": "^0.1.4",
"gulp-istanbul": "^1.1.1",
"gulp-mocha": "^3.0.1",
"gulp-sourcemaps": "1.9.1",
"gulp-typescript": "^3.1.3",
"gulp-util": "3.0.8",
"jsdoc": "3.4.3",
"jsdom": "^11.1.0",
"merge-stream": "^1.0.1",
"mocha": "2.5.3",
"node-pty": "^0.4.1",
"nodemon": "1.10.2",
"sleep": "^3.0.1",
"sorcery": "^0.10.0",
"tsify": "^3.0.0",
"tslint": "^4.0.2",
"typescript": "^2.0.3",
"typescript": "~2.2.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0"
},
"scripts": {
"prestart": "npm run build",
"start": "node demo/app",
"dev": "nodemon -e js,ts --watch src --watch demo --exec npm start",
"dev": "nodemon -e js,ts,css --watch src --watch demo --exec npm start",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "mocha --recursive ./lib",
"test": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"build": "gulp build",
"prepublish": "npm run build"
"prepublish": "npm run build",
"coveralls": "gulp coveralls"
}
}
+31
View File
@@ -0,0 +1,31 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
describe('Buffer', () => {
let terminal: ITerminal;
let buffer: Buffer;
beforeEach(() => {
terminal = <any>{
cols: 80,
rows: 24,
scrollback: 1000
};
buffer = new Buffer(terminal);
});
describe('constructor', () => {
it('should create a CircularList with max length equal to scrollback, for its lines', () => {
assert.instanceOf(buffer.lines, CircularList);
assert.equal(buffer.lines.maxLength, terminal.scrollback);
});
it('should set the Buffer\'s scrollBottom value equal to the terminal\'s rows -1', () => {
assert.equal(buffer.scrollBottom, terminal.rows - 1);
});
});
});
+42
View File
@@ -0,0 +1,42 @@
/**
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { CircularList } from './utils/CircularList';
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level):
* - text content of this particular buffer
* - cursor position
* - scroll position
*/
export class Buffer {
public lines: CircularList<[number, string, number][]>;
public savedY: number;
public savedX: number;
/**
* Create a new Buffer.
* @param {Terminal} terminal - The terminal the Buffer will belong to
* @param {number} ydisp - The scroll position of the Buffer in the viewport
* @param {number} ybase - The scroll position of the y cursor (ybase + y = the y position within the Buffer)
* @param {number} y - The cursor's y position after ybase
* @param {number} x - The cursor's x position after ybase
*/
constructor(
private terminal: ITerminal,
public ydisp: number = 0,
public ybase: number = 0,
public y: number = 0,
public x: number = 0,
public scrollBottom: number = 0,
public scrollTop: number = 0,
public tabs: any = {},
) {
this.lines = new CircularList<[number, string, number][]>(this.terminal.scrollback);
this.scrollBottom = this.terminal.rows - 1;
}
}
+49
View File
@@ -0,0 +1,49 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer';
describe('BufferSet', () => {
let terminal: ITerminal;
let bufferSet: BufferSet;
beforeEach(() => {
terminal = <any>{
cols: 80,
rows: 24,
scrollback: 1000
};
bufferSet = new BufferSet(terminal);
});
describe('constructor', () => {
it('should create two different buffers: alt and normal', () => {
assert.instanceOf(bufferSet.normal, Buffer);
assert.instanceOf(bufferSet.alt, Buffer);
assert.notEqual(bufferSet.normal, bufferSet.alt);
});
});
describe('activateNormalBuffer', () => {
beforeEach(() => {
bufferSet.activateNormalBuffer();
});
it('should set the normal buffer as the currently active buffer', () => {
assert.equal(bufferSet.active, bufferSet.normal);
});
});
describe('activateAltBuffer', () => {
beforeEach(() => {
bufferSet.activateAltBuffer();
});
it('should set the alt buffer as the currently active buffer', () => {
assert.equal(bufferSet.active, bufferSet.alt);
});
});
});
+68
View File
@@ -0,0 +1,68 @@
/**
* @license MIT
*/
import { ITerminal, IBufferSet } from './Interfaces';
import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter';
/**
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
* provides also utilities for working with them.
*/
export class BufferSet extends EventEmitter implements IBufferSet {
private _normal: Buffer;
private _alt: Buffer;
private _activeBuffer: Buffer;
/**
* Create a new BufferSet for the given terminal.
* @param {Terminal} terminal - The terminal the BufferSet will belong to
*/
constructor(private _terminal: ITerminal) {
super();
this._normal = new Buffer(this._terminal);
this._alt = new Buffer(this._terminal);
this._activeBuffer = this._normal;
}
/**
* Returns the alt Buffer of the BufferSet
* @returns {Buffer}
*/
public get alt(): Buffer {
return this._alt;
}
/**
* Returns the normal Buffer of the BufferSet
* @returns {Buffer}
*/
public get active(): Buffer {
return this._activeBuffer;
}
/**
* Returns the currently active Buffer of the BufferSet
* @returns {Buffer}
*/
public get normal(): Buffer {
return this._normal;
}
/**
* Sets the normal Buffer of the BufferSet as its currently active Buffer
*/
public activateNormalBuffer(): void {
this._activeBuffer = this._normal;
this.emit('activate', this._normal);
}
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
this._activeBuffer = this._alt;
this.emit('activate', this._alt);
}
}
+4
View File
@@ -1,3 +1,7 @@
/**
* @license MIT
*/
/**
* C0 control codes
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
+4 -5
View File
@@ -2,12 +2,14 @@
* @license MIT
*/
import { IEventEmitter } from './Interfaces';
interface ListenerType {
(): void;
listener?: () => void;
};
export class EventEmitter {
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: ListenerType[]};
constructor() {
@@ -53,14 +55,11 @@ export class EventEmitter {
return this.on(type, on);
}
public emit(type): void {
public emit(type: string, ...args: any[]): void {
if (!this._events[type]) {
return;
}
let args = Array.prototype.slice.call(arguments, 1);
let obj = this._events[type];
for (let i = 0; i < obj.length; i++) {
obj[i].apply(this, args);
}
+152
View File
@@ -1,7 +1,23 @@
import { assert } from 'chai';
import { InputHandler } from './InputHandler';
import { wcwidth } from './InputHandler';
describe('InputHandler', () => {
describe('save and restore cursor', () => {
let terminal = { buffer: { x: 1, y: 2 } };
let inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
let options = {};
@@ -47,3 +63,139 @@ describe('InputHandler', () => {
});
});
});
const old_wcwidth = (function(opts) {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const 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],
[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) {
let min = 0;
let max = COMBINING.length - 1;
let mid;
if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1])
return false;
while (max >= min) {
mid = Math.floor((min + max) / 2);
if (ucs > COMBINING[mid][1])
min = mid + 1;
else if (ucs < COMBINING[mid][0])
max = mid - 1;
else
return true;
}
return false;
}
function wcwidth(ucs) {
// test for 8-bit control characters
if (ucs === 0)
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return opts.control;
// binary search in table of non-spacing characters
if (bisearch(ucs))
return 0;
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWide(ucs)) {
return 2;
}
return 1;
}
function isWide(ucs) {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
(ucs >= 0x30000 && ucs <= 0x3fffd)));
}
return wcwidth;
})({nul: 0, control: 0}); // configurable options
describe('wcwidth', () => {
it('same as old implementation for BMP and individual higher', (done) => {
for (let i = 0; i < 65536; ++i)
assert.equal(wcwidth(i), old_wcwidth(i));
// test some individual higher to fullfill branching
assert.equal(wcwidth(0x10A01), old_wcwidth(0x10A01));
assert.equal(wcwidth(0x30000), old_wcwidth(0x30000));
assert.equal(wcwidth(0x3fffe), old_wcwidth(0x3fffe));
done();
}).timeout(3000);
/*
it('new is at least 5 times faster', () => {
let start_new = new Date().getTime();
let x = 0;
for (let runs = 0; runs < 1; ++runs)
for (let i = 0; i < 65536; ++i)
x = wcwidth(i);
let end_new = new Date().getTime();
let start_old = new Date().getTime();
let y = 0;
for (let runs = 0; runs < 1; ++runs)
for (let i = 0; i < 65536; ++i)
y = old_wcwidth(i);
let end_old = new Date().getTime();
// console.log((end_new - start_new));
// console.log((end_old - start_old));
assert.equal(((end_new - start_new) * 5 < (end_old - start_old)), true);
});
*/
});
+339 -289
View File
@@ -27,44 +27,48 @@ export class InputHandler implements IInputHandler {
char = this._terminal.charset[char];
}
let row = this._terminal.y + this._terminal.ybase;
let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
// insert combining char in last cell
// FIXME: needs handling after cursor jumps
if (!ch_width && this._terminal.x) {
if (!ch_width && this._terminal.buffer.x) {
// dont overflow left
if (this._terminal.lines.get(row)[this._terminal.x - 1]) {
if (!this._terminal.lines.get(row)[this._terminal.x - 1][2]) {
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][2]) {
// found empty cell after fullwidth, need to go 2 cells back
if (this._terminal.lines.get(row)[this._terminal.x - 2])
this._terminal.lines.get(row)[this._terminal.x - 2][1] += char;
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2])
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][1] += char;
} else {
this._terminal.lines.get(row)[this._terminal.x - 1][1] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][1] += char;
}
this._terminal.updateRange(this._terminal.y);
this._terminal.updateRange(this._terminal.buffer.y);
}
return;
}
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (this._terminal.x + ch_width - 1 >= this._terminal.cols) {
if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
this._terminal.x = 0;
this._terminal.y++;
if (this._terminal.y > this._terminal.scrollBottom) {
this._terminal.y--;
this._terminal.scroll();
this._terminal.buffer.x = 0;
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
this._terminal.scroll(true);
} else {
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
this._terminal.buffer.lines.get(this._terminal.buffer.y).isWrapped = true;
}
} else {
if (ch_width === 2) // FIXME: check for xterm behavior
return;
}
}
row = this._terminal.y + this._terminal.ybase;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
// insert mode: move characters to right
if (this._terminal.insertMode) {
@@ -72,25 +76,26 @@ export class InputHandler implements IInputHandler {
for (let moves = 0; moves < ch_width; ++moves) {
// remove last cell, if it's width is 0
// we have to adjust the second last cell as well
const removed = this._terminal.lines.get(this._terminal.y + this._terminal.ybase).pop();
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
if (removed[2] === 0
&& this._terminal.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.lines.get(row)[this._terminal.cols - 2][2] === 2)
this._terminal.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][2] === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1];
}
// insert empty cell at cursor
this._terminal.lines.get(row).splice(this._terminal.x, 0, [this._terminal.curAttr, ' ', 1]);
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1]);
}
}
this._terminal.lines.get(row)[this._terminal.x] = [this._terminal.curAttr, char, ch_width];
this._terminal.x++;
this._terminal.updateRange(this._terminal.y);
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (ch_width === 2) {
this._terminal.lines.get(row)[this._terminal.x] = [this._terminal.curAttr, '', 0];
this._terminal.x++;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0];
this._terminal.buffer.x++;
}
}
}
@@ -116,17 +121,23 @@ export class InputHandler implements IInputHandler {
*/
public lineFeed(): void {
if (this._terminal.convertEol) {
this._terminal.x = 0;
this._terminal.buffer.x = 0;
}
this._terminal.y++;
if (this._terminal.y > this._terminal.scrollBottom) {
this._terminal.y--;
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
this._terminal.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x--;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
/**
* This event is emitted whenever the terminal outputs a LF or NL.
*
* @event lineFeed
*/
this._terminal.emit('lineFeed');
}
/**
@@ -134,7 +145,7 @@ export class InputHandler implements IInputHandler {
* Carriage Return (Ctrl-M).
*/
public carriageReturn(): void {
this._terminal.x = 0;
this._terminal.buffer.x = 0;
}
/**
@@ -142,8 +153,8 @@ export class InputHandler implements IInputHandler {
* Backspace (Ctrl-H).
*/
public backspace(): void {
if (this._terminal.x > 0) {
this._terminal.x--;
if (this._terminal.buffer.x > 0) {
this._terminal.buffer.x--;
}
}
@@ -152,7 +163,7 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
this._terminal.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.nextStop();
}
/**
@@ -183,13 +194,13 @@ export class InputHandler implements IInputHandler {
param = params[0];
if (param < 1) param = 1;
row = this._terminal.y + this._terminal.ybase;
j = this._terminal.x;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
j = this._terminal.buffer.x;
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.lines.get(row).splice(j++, 0, ch);
this._terminal.lines.get(row).pop();
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
this._terminal.buffer.lines.get(row).pop();
}
}
@@ -202,9 +213,9 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y -= param;
if (this._terminal.y < 0) {
this._terminal.y = 0;
this._terminal.buffer.y -= param;
if (this._terminal.buffer.y < 0) {
this._terminal.buffer.y = 0;
}
}
@@ -217,13 +228,13 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y += param;
if (this._terminal.y >= this._terminal.rows) {
this._terminal.y = this._terminal.rows - 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x--;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
}
@@ -236,9 +247,9 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.x += param;
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x = this._terminal.cols - 1;
this._terminal.buffer.x += param;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
@@ -252,12 +263,12 @@ export class InputHandler implements IInputHandler {
param = 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x--;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
this._terminal.x -= param;
if (this._terminal.x < 0) {
this._terminal.x = 0;
this._terminal.buffer.x -= param;
if (this._terminal.buffer.x < 0) {
this._terminal.buffer.x = 0;
}
}
@@ -271,12 +282,12 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y += param;
if (this._terminal.y >= this._terminal.rows) {
this._terminal.y = this._terminal.rows - 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
this._terminal.x = 0;
};
this._terminal.buffer.x = 0;
}
/**
@@ -289,12 +300,12 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y -= param;
if (this._terminal.y < 0) {
this._terminal.y = 0;
this._terminal.buffer.y -= param;
if (this._terminal.buffer.y < 0) {
this._terminal.buffer.y = 0;
}
this._terminal.x = 0;
};
this._terminal.buffer.x = 0;
}
/**
@@ -306,7 +317,7 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.x = param - 1;
this._terminal.buffer.x = param - 1;
}
/**
@@ -336,8 +347,8 @@ export class InputHandler implements IInputHandler {
col = this._terminal.cols - 1;
}
this._terminal.x = col;
this._terminal.y = row;
this._terminal.buffer.x = col;
this._terminal.buffer.y = row;
}
/**
@@ -347,7 +358,7 @@ export class InputHandler implements IInputHandler {
public cursorForwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.x = this._terminal.nextStop();
this._terminal.buffer.x = this._terminal.nextStop();
}
}
@@ -367,15 +378,15 @@ export class InputHandler implements IInputHandler {
let j;
switch (params[0]) {
case 0:
this._terminal.eraseRight(this._terminal.x, this._terminal.y);
j = this._terminal.y + 1;
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y + 1;
for (; j < this._terminal.rows; j++) {
this._terminal.eraseLine(j);
}
break;
case 1:
this._terminal.eraseLeft(this._terminal.x, this._terminal.y);
j = this._terminal.y;
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
j = this._terminal.buffer.y;
while (j--) {
this._terminal.eraseLine(j);
}
@@ -385,7 +396,15 @@ export class InputHandler implements IInputHandler {
while (j--) this._terminal.eraseLine(j);
break;
case 3:
; // no saved lines
// Clear scrollback (everything not in viewport)
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
if (scrollBackSize > 0) {
this._terminal.buffer.lines.trimStart(scrollBackSize);
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
// Force a scroll event to refresh viewport
this._terminal.emit('scroll', 0);
}
break;
}
}
@@ -404,13 +423,13 @@ export class InputHandler implements IInputHandler {
public eraseInLine(params: number[]): void {
switch (params[0]) {
case 0:
this._terminal.eraseRight(this._terminal.x, this._terminal.y);
this._terminal.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 1:
this._terminal.eraseLeft(this._terminal.x, this._terminal.y);
this._terminal.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
break;
case 2:
this._terminal.eraseLine(this._terminal.y);
this._terminal.eraseLine(this._terminal.buffer.y);
break;
}
}
@@ -426,29 +445,29 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
row = this._terminal.y + this._terminal.ybase;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
j = this._terminal.rows - 1 - this._terminal.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.ybase - j + 1;
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j + 1;
while (param--) {
if (this._terminal.lines.length === this._terminal.lines.maxLength) {
if (this._terminal.buffer.lines.length === this._terminal.buffer.lines.maxLength) {
// Trim the start of lines to make room for the new line
this._terminal.lines.trimStart(1);
this._terminal.ybase--;
this._terminal.ydisp--;
this._terminal.buffer.lines.trimStart(1);
this._terminal.buffer.ybase--;
this._terminal.buffer.ydisp--;
row--;
j--;
}
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.lines.splice(row, 0, this._terminal.blankLine(true));
this._terminal.lines.splice(j, 1);
this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true));
this._terminal.buffer.lines.splice(j, 1);
}
// this.maxRange();
this._terminal.updateRange(this._terminal.y);
this._terminal.updateRange(this._terminal.scrollBottom);
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
}
/**
@@ -462,27 +481,27 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
row = this._terminal.y + this._terminal.ybase;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
j = this._terminal.rows - 1 - this._terminal.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.ybase - j;
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j;
while (param--) {
if (this._terminal.lines.length === this._terminal.lines.maxLength) {
if (this._terminal.buffer.lines.length === this._terminal.buffer.lines.maxLength) {
// Trim the start of lines to make room for the new line
this._terminal.lines.trimStart(1);
this._terminal.ybase -= 1;
this._terminal.ydisp -= 1;
this._terminal.buffer.lines.trimStart(1);
this._terminal.buffer.ybase -= 1;
this._terminal.buffer.ydisp -= 1;
}
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.lines.splice(j + 1, 0, this._terminal.blankLine(true));
this._terminal.lines.splice(row, 1);
this._terminal.buffer.lines.splice(j + 1, 0, this._terminal.blankLine(true));
this._terminal.buffer.lines.splice(row, 1);
}
// this.maxRange();
this._terminal.updateRange(this._terminal.y);
this._terminal.updateRange(this._terminal.scrollBottom);
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
}
/**
@@ -497,12 +516,12 @@ export class InputHandler implements IInputHandler {
param = 1;
}
row = this._terminal.y + this._terminal.ybase;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
while (param--) {
this._terminal.lines.get(row).splice(this._terminal.x, 1);
this._terminal.lines.get(row).push(ch);
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
this._terminal.buffer.lines.get(row).push(ch);
}
}
@@ -512,12 +531,12 @@ export class InputHandler implements IInputHandler {
public scrollUp(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollTop, 1);
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollBottom, 0, this._terminal.blankLine());
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.scrollTop);
this._terminal.updateRange(this._terminal.scrollBottom);
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
}
/**
@@ -526,12 +545,12 @@ export class InputHandler implements IInputHandler {
public scrollDown(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollBottom, 1);
this._terminal.lines.splice(this._terminal.ybase + this._terminal.scrollTop, 0, this._terminal.blankLine());
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.scrollTop);
this._terminal.updateRange(this._terminal.scrollBottom);
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
}
/**
@@ -546,12 +565,12 @@ export class InputHandler implements IInputHandler {
param = 1;
}
row = this._terminal.y + this._terminal.ybase;
j = this._terminal.x;
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
j = this._terminal.buffer.x;
ch = [this._terminal.eraseAttr(), ' ', 1]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.lines.get(row)[j++] = ch;
this._terminal.buffer.lines.get(row)[j++] = ch;
}
}
@@ -561,7 +580,7 @@ export class InputHandler implements IInputHandler {
public cursorBackwardTab(params: number[]): void {
let param = params[0] || 1;
while (param--) {
this._terminal.x = this._terminal.prevStop();
this._terminal.buffer.x = this._terminal.prevStop();
}
}
@@ -574,9 +593,9 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.x = param - 1;
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x = this._terminal.cols - 1;
this._terminal.buffer.x = param - 1;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
@@ -590,9 +609,9 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.x += param;
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x = this._terminal.cols - 1;
this._terminal.buffer.x += param;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
@@ -601,11 +620,11 @@ export class InputHandler implements IInputHandler {
*/
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1
, line = this._terminal.lines.get(this._terminal.ybase + this._terminal.y)
, ch = line[this._terminal.x - 1] || [this._terminal.defAttr, ' ', 1];
, line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y)
, ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1];
while (param--) {
line[this._terminal.x++] = ch;
line[this._terminal.buffer.x++] = ch;
}
}
@@ -684,9 +703,9 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y = param - 1;
if (this._terminal.y >= this._terminal.rows) {
this._terminal.y = this._terminal.rows - 1;
this._terminal.buffer.y = param - 1;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
}
@@ -700,13 +719,13 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
this._terminal.y += param;
if (this._terminal.y >= this._terminal.rows) {
this._terminal.y = this._terminal.rows - 1;
this._terminal.buffer.y += param;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x--;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
}
}
@@ -719,14 +738,14 @@ export class InputHandler implements IInputHandler {
if (params[0] < 1) params[0] = 1;
if (params[1] < 1) params[1] = 1;
this._terminal.y = params[0] - 1;
if (this._terminal.y >= this._terminal.rows) {
this._terminal.y = this._terminal.rows - 1;
this._terminal.buffer.y = params[0] - 1;
if (this._terminal.buffer.y >= this._terminal.rows) {
this._terminal.buffer.y = this._terminal.rows - 1;
}
this._terminal.x = params[1] - 1;
if (this._terminal.x >= this._terminal.cols) {
this._terminal.x = this._terminal.cols - 1;
this._terminal.buffer.x = params[1] - 1;
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x = this._terminal.cols - 1;
}
}
@@ -741,9 +760,9 @@ export class InputHandler implements IInputHandler {
public tabClear(params: number[]): void {
let param = params[0];
if (param <= 0) {
delete this._terminal.tabs[this._terminal.x];
delete this._terminal.buffer.tabs[this._terminal.buffer.x];
} else if (param === 3) {
this._terminal.tabs = {};
this._terminal.buffer.tabs = {};
}
}
@@ -897,7 +916,8 @@ export class InputHandler implements IInputHandler {
this._terminal.vt200Mouse = params[0] === 1000;
this._terminal.normalMouse = params[0] > 1000;
this._terminal.mouseEvents = true;
this._terminal.element.style.cursor = 'default';
this._terminal.element.classList.add('enable-mouse-events');
this._terminal.selectionManager.disable();
this._terminal.log('Binding to mouse events.');
break;
case 1004: // send focusin/focusout events
@@ -928,30 +948,15 @@ export class InputHandler implements IInputHandler {
this._terminal.cursorHidden = false;
break;
case 1049: // alt screen buffer cursor
// this._terminal.saveCursor();
; // FALL-THROUGH
// TODO: Not sure if we need to save/restore after switching the buffer
// this.saveCursor(params);
// FALL-THROUGH
case 47: // alt screen buffer
case 1047: // alt screen buffer
if (!this._terminal.normal) {
let normal = {
lines: this._terminal.lines,
ybase: this._terminal.ybase,
ydisp: this._terminal.ydisp,
x: this._terminal.x,
y: this._terminal.y,
scrollTop: this._terminal.scrollTop,
scrollBottom: this._terminal.scrollBottom,
tabs: this._terminal.tabs
// XXX save charset(s) here?
// charset: this._terminal.charset,
// glevel: this._terminal.glevel,
// charsets: this._terminal.charsets
};
this._terminal.reset();
this._terminal.viewport.syncScrollArea();
this._terminal.normal = normal;
this._terminal.showCursor();
}
this._terminal.buffers.activateAltBuffer();
this._terminal.reset();
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
}
}
@@ -1090,7 +1095,8 @@ export class InputHandler implements IInputHandler {
this._terminal.vt200Mouse = false;
this._terminal.normalMouse = false;
this._terminal.mouseEvents = false;
this._terminal.element.style.cursor = '';
this._terminal.element.classList.remove('enable-mouse-events');
this._terminal.selectionManager.enable();
break;
case 1004: // send focusin/focusout events
this._terminal.sendFocus = false;
@@ -1108,27 +1114,19 @@ export class InputHandler implements IInputHandler {
this._terminal.cursorHidden = true;
break;
case 1049: // alt screen buffer cursor
; // FALL-THROUGH
// FALL-THROUGH
case 47: // normal screen buffer
case 1047: // normal screen buffer - clearing it first
if (this._terminal.normal) {
this._terminal.lines = this._terminal.normal.lines;
this._terminal.ybase = this._terminal.normal.ybase;
this._terminal.ydisp = this._terminal.normal.ydisp;
this._terminal.x = this._terminal.normal.x;
this._terminal.y = this._terminal.normal.y;
this._terminal.scrollTop = this._terminal.normal.scrollTop;
this._terminal.scrollBottom = this._terminal.normal.scrollBottom;
this._terminal.tabs = this._terminal.normal.tabs;
this._terminal.normal = null;
// if (params === 1049) {
// this.x = this.savedX;
// this.y = this.savedY;
// }
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
}
// Ensure the selection manager has the correct buffer
this._terminal.buffers.activateNormalBuffer();
// TODO: Not sure if we need to save/restore after switching the buffer
// if (params[0] === 1049) {
// this.restoreCursor(params);
// }
this._terminal.selectionManager.setBuffer(this._terminal.buffer.lines);
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
}
}
@@ -1348,9 +1346,9 @@ export class InputHandler implements IInputHandler {
case 6:
// cursor position
this._terminal.send(C0.ESC + '['
+ (this._terminal.y + 1)
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.x + 1)
+ (this._terminal.buffer.x + 1)
+ 'R');
break;
}
@@ -1361,9 +1359,9 @@ export class InputHandler implements IInputHandler {
case 6:
// cursor position
this._terminal.send(C0.ESC + '[?'
+ (this._terminal.y + 1)
+ (this._terminal.buffer.y + 1)
+ ';'
+ (this._terminal.x + 1)
+ (this._terminal.buffer.x + 1)
+ 'R');
break;
case 15:
@@ -1394,14 +1392,14 @@ export class InputHandler implements IInputHandler {
this._terminal.cursorHidden = false;
this._terminal.insertMode = false;
this._terminal.originMode = false;
this._terminal.wraparoundMode = false; // autowrap
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this._terminal.applicationKeypad = false; // ?
this._terminal.viewport.syncScrollArea();
this._terminal.applicationCursor = false;
this._terminal.scrollTop = 0;
this._terminal.scrollBottom = this._terminal.rows - 1;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
this._terminal.curAttr = this._terminal.defAttr;
this._terminal.x = this._terminal.y = 0; // ?
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
this._terminal.charset = null;
this._terminal.glevel = 0; // ??
this._terminal.charsets = [null]; // ??
@@ -1445,10 +1443,10 @@ export class InputHandler implements IInputHandler {
*/
public setScrollRegion(params: number[]): void {
if (this._terminal.prefix) return;
this._terminal.scrollTop = (params[0] || 1) - 1;
this._terminal.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
this._terminal.x = 0;
this._terminal.y = 0;
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
this._terminal.buffer.x = 0;
this._terminal.buffer.y = 0;
}
@@ -1457,8 +1455,8 @@ export class InputHandler implements IInputHandler {
* Save cursor (ANSI.SYS).
*/
public saveCursor(params: number[]): void {
this._terminal.savedX = this._terminal.x;
this._terminal.savedY = this._terminal.y;
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
}
@@ -1467,100 +1465,102 @@ export class InputHandler implements IInputHandler {
* Restore cursor (ANSI.SYS).
*/
public restoreCursor(params: number[]): void {
this._terminal.x = this._terminal.savedX || 0;
this._terminal.y = this._terminal.savedY || 0;
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
}
}
const wcwidth = (function(opts) {
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
// combining characters
const 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],
[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) {
let min = 0;
let max = COMBINING.length - 1;
let mid;
if (ucs < COMBINING[0][0] || ucs > COMBINING[max][1])
export const wcwidth = (function(opts) {
// 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, data) {
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;
while (max >= min) {
mid = Math.floor((min + max) / 2);
if (ucs > COMBINING[mid][1])
min = mid + 1;
else if (ucs < COMBINING[mid][0])
max = mid - 1;
else
return true;
}
return false;
}
function wcwidth(ucs) {
// test for 8-bit control characters
if (ucs === 0)
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return opts.control;
// binary search in table of non-spacing characters
if (bisearch(ucs))
return 0;
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWide(ucs)) {
return 2;
function wcwidthBMP(ucs) {
// test for 8-bit control characters
if (ucs === 0)
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
return opts.control;
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
return 0;
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
}
return 1;
}
return 1;
}
function isWide(ucs) {
return (
ucs >= 0x1100 && (
function isWideBMP(ucs) {
return (
ucs >= 0x1100 && (
ucs <= 0x115f || // Hangul Jamo init. consonants
ucs === 0x2329 ||
ucs === 0x232a ||
@@ -1570,9 +1570,59 @@ const wcwidth = (function(opts) {
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
(ucs >= 0xffe0 && ucs <= 0xffe6) ||
(ucs >= 0x20000 && ucs <= 0x2fffd) ||
(ucs >= 0x30000 && ucs <= 0x3fffd)));
}
return wcwidth;
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs) {
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;
let table = null;
function init_table() {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
const ITEMSIZE = 32; // using uint32_t
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
table = (typeof Uint32Array === 'undefined')
? new Array(CONTAINERSIZE)
: new Uint32Array(CONTAINERSIZE);
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
table[i] = num;
}
return table;
}
// get width from lookup table
// position in container : num / CODEPOINTS_PER_ITEM
// ==> n = table[Math.floor(num / 16)]
// ==> n = table[num >> 4]
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
// ==> m = (n % 16) * 2
// ==> m = (num & 15) << 1
// right shift to position m
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
// we are only interested in 2 LSBs, cut off higher bits
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num) {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
return control | 0;
if (num < 127)
return 1;
let t = table || init_table();
if (num < 65536)
return t[num >> 4] >> ((num & 15) << 1) & 3;
// do a full search for high codepoints
return wcwidthHigh(num);
};
})({nul: 0, control: 0}); // configurable options
+75 -6
View File
@@ -2,6 +2,9 @@
* @license MIT
*/
import { LinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
export interface IBrowser {
isNode: boolean;
userAgent: string;
@@ -17,10 +20,10 @@ export interface IBrowser {
export interface ITerminal {
element: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
ybase: number;
ydisp: number;
lines: ICircularList<string>;
rows: number;
cols: number;
browser: IBrowser;
@@ -28,9 +31,10 @@ export interface ITerminal {
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
x: number;
y: number;
defAttr: number;
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
/**
* Emit the 'data' event and populate the given data.
@@ -42,9 +46,50 @@ export interface ITerminal {
cancel(ev: Event, force?: boolean);
log(text: string): void;
emit(event: string, data: any);
reset(): void;
showCursor(): void;
}
interface ICircularList<T> {
export interface IBuffer {
lines: ICircularList<[number, string, number][]>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
}
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
setSelection(row: number, col: number, length: number);
}
export interface ICharMeasure {
width: number;
height: number;
measure(): void;
}
export interface ILinkifier {
linkifyRow(rowIndex: number): void;
attachHypertextLinkHandler(handler: LinkMatcherHandler): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: LinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
@@ -58,6 +103,30 @@ interface ICircularList<T> {
shiftElements(start: number, count: number, offset: number): void;
}
export interface IEventEmitter {
on(type, listener): void;
off(type, listener): void;
}
export interface LinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
}
/**
* Handles actions generated by the parser.
*/
+192
View File
@@ -0,0 +1,192 @@
/**
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ILinkifier } from './Interfaces';
import { Linkifier } from './Linkifier';
import { LinkMatcher } from './Types';
class TestLinkifier extends Linkifier {
constructor() {
Linkifier.TIME_BEFORE_LINKIFY = 0;
super();
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
}
describe('Linkifier', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let container: HTMLElement;
let rows: HTMLElement[];
let linkifier: TestLinkifier;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
linkifier = new TestLinkifier();
});
function addRow(html: string) {
const element = document.createElement('div');
element.innerHTML = html;
container.appendChild(element);
rows.push(element);
}
describe('before attachToDom', () => {
it('should allow link matcher registration', done => {
assert.doesNotThrow(() => {
const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {});
assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId));
done();
});
});
});
describe('after attachToDom', () => {
beforeEach(() => {
rows = [];
linkifier.attachToDom(document, rows);
container = document.createElement('div');
document.body.appendChild(container);
});
function clickElement(element: Node) {
const event = document.createEvent('MouseEvent');
event.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
element.dispatchEvent(event);
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
describe('http links', () => {
function assertLinkifiesEntireRow(uri: string, done: MochaDone) {
addRow(uri);
linkifier.linkifyRow(0);
setTimeout(() => {
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
assert.equal((<HTMLElement>rows[0].firstChild).textContent, uri);
done();
}, 0);
}
it('should allow ~ character in URI path', done => assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done));
});
describe('link matcher', () => {
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, expectedHtml: string, done: MochaDone) {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRow(0);
// Allow linkify to happen
setTimeout(() => {
assert.equal(rows[0].innerHTML, expectedHtml);
done();
}, 0);
}
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, '<a>foo</a>', done);
});
it('should match a single link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo/, '<a>foo</a> bar', done);
});
it('should match a single link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar/, 'foo <a>bar</a> baz', done);
});
it('should match a single link at the end of a text node', done => {
assertLinkifiesRow('foo bar', /bar/, 'foo <a>bar</a>', done);
});
it('should match a link after a link at the start of a text node', done => {
assertLinkifiesRow('foo bar', /foo|bar/, '<a>foo</a> <a>bar</a>', done);
});
it('should match a link after a link in the middle of a text node', done => {
assertLinkifiesRow('foo bar baz', /bar|baz/, 'foo <a>bar</a> <a>baz</a>', done);
});
it('should match a link immediately after a link at the end of a text node', done => {
assertLinkifiesRow('<span>foo bar</span>baz', /bar|baz/, '<span>foo <a>bar</a></span><a>baz</a>', done);
});
it('should not duplicate text after a unicode character (wrapped in a span)', done => {
// This is a regression test for an issue that came about when using
// an oh-my-zsh theme that added the large blue diamond unicode
// character (U+1F537) which caused the path to be duplicated. See #642.
assertLinkifiesRow('echo \'<span class="xterm-normal-char">🔷</span>foo\'', /foo/, 'echo \'<span class="xterm-normal-char">🔷</span><a>foo</a>\'', done);
});
});
describe('validationCallback', () => {
it('should enable link if true', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => done(), {
validationCallback: (url, element, cb) => {
cb(true);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
});
linkifier.linkifyRow(0);
});
it('should disable link if false', done => {
addRow('test');
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
cb(false);
assert.equal((<HTMLElement>rows[0].firstChild).tagName, 'A');
setTimeout(() => clickElement(rows[0].firstChild), 0);
}
});
linkifier.linkifyRow(0);
// Allow time for the click to be performed
setTimeout(() => done(), 10);
});
it('should trigger for multiple link matches on one row', done => {
addRow('test test');
let count = 0;
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
validationCallback: (url, element, cb) => {
count += 1;
if (count === 2) {
done();
}
cb(false);
}
});
linkifier.linkifyRow(0);
});
});
describe('priority', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
});
});
});
});
+363
View File
@@ -0,0 +1,363 @@
/**
* @license MIT
*/
import { LinkMatcherOptions } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback } from './Types';
const INVALID_LINK_CLASS = 'xterm-invalid-link';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
/**
* The ID of the built in http(s) link matcher.
*/
const HYPERTEXT_LINK_MATCHER_ID = 0;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
export class Linkifier {
/**
* The time to wait after a row is changed before it is linkified. This prevents
* the costly operation of searching every row multiple times, potentially a
* huge amount of times.
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: LinkMatcher[];
private _document: Document;
private _rows: HTMLElement[];
private _rowTimeoutIds: number[];
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
constructor() {
this._rowTimeoutIds = [];
this._linkMatchers = [];
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
* Attaches the linkifier to the DOM, enabling linkification.
* @param document The document object.
* @param rows The array of rows to apply links to.
*/
public attachToDom(document: Document, rows: HTMLElement[]) {
this._document = document;
this._rows = rows;
}
/**
* Queues a row for linkification.
* @param {number} rowIndex The index of the row to linkify.
*/
public linkifyRow(rowIndex: number): void {
// Don't attempt linkify if not yet attached to DOM
if (!this._document) {
return;
}
const timeoutId = this._rowTimeoutIds[rowIndex];
if (timeoutId) {
clearTimeout(timeoutId);
}
this._rowTimeoutIds[rowIndex] = setTimeout(this._linkifyRow.bind(this, rowIndex), Linkifier.TIME_BEFORE_LINKIFY);
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior
* for standard http(s) links.
* @param {LinkHandler} handler The handler to use, this can be cleared with
* null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
}
/**
* Attaches a validation callback for hypertext links.
* @param {LinkMatcherValidationCallback} callback The callback to use, this
* can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
* @param {RegExp} regex The regular expression to search for, specifically
* this searches the textContent of the rows. You will want to use \s to match
* a space ' ' character for example.
* @param {LinkHandler} handler The callback when the link is called.
* @param {LinkMatcherOptions} [options] Options for the link matcher.
* @return {number} The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: LinkMatcherOptions = {}): number {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
throw new Error('handler must be defined');
}
const matcher: LinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
matchIndex: options.matchIndex,
validationCallback: options.validationCallback,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
return matcher.id;
}
/**
* Inserts a link matcher to the list in the correct position based on the
* priority of each link matcher. New link matchers of equal priority are
* considered after older link matchers.
* @param matcher The link matcher to be added.
*/
private _addLinkMatcherToList(matcher: LinkMatcher): void {
if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher);
return;
}
for (let i = this._linkMatchers.length - 1; i >= 0; i--) {
if (matcher.priority <= this._linkMatchers[i].priority) {
this._linkMatchers.splice(i + 1, 0, matcher);
return;
}
}
this._linkMatchers.splice(0, 0, matcher);
}
/**
* Deregisters a link matcher if it has been registered.
* @param {number} matcherId The link matcher's ID (returned after register)
* @return {boolean} Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
}
}
return false;
}
/**
* Linkifies a row.
* @param {number} rowIndex The index of the row to linkify.
*/
private _linkifyRow(rowIndex: number): void {
const row = this._rows[rowIndex];
if (!row) {
return;
}
const text = row.textContent;
for (let i = 0; i < this._linkMatchers.length; i++) {
const matcher = this._linkMatchers[i];
const linkElements = this._doLinkifyRow(row, matcher);
if (linkElements.length > 0) {
// Fire validation callback
if (matcher.validationCallback) {
for (let j = 0; j < linkElements.length; j++) {
const element = linkElements[j];
matcher.validationCallback(element.textContent, element, isValid => {
if (!isValid) {
element.classList.add(INVALID_LINK_CLASS);
}
});
}
}
// Only allow a single LinkMatcher to trigger on any given row.
return;
}
}
}
/**
* Linkifies a row given a specific handler.
* @param {HTMLElement} row The row to linkify.
* @param {LinkMatcher} matcher The link matcher for this line.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(row: HTMLElement, matcher: LinkMatcher): HTMLElement[] {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
const nodes = row.childNodes;
// Find the first match
let match = row.textContent.match(matcher.regex);
if (!match || match.length === 0) {
return result;
}
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Set the next searches start index
let rowStartIndex = match.index + uri.length;
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const searchIndex = node.textContent.indexOf(uri);
if (searchIndex >= 0) {
const linkElement = this._createAnchorElement(uri, matcher.handler, isHttpLinkMatcher);
if (node.textContent.length === uri.length) {
// Matches entire string
if (node.nodeType === 3 /*Node.TEXT_NODE*/) {
this._replaceNode(node, linkElement);
} else {
const element = (<HTMLElement>node);
if (element.nodeName === 'A') {
// This row has already been linkified
return result;
}
element.innerHTML = '';
element.appendChild(linkElement);
}
} else if (node.childNodes.length > 1) {
// Matches part of string in an element with multiple child nodes
for (let j = 0; j < node.childNodes.length; j++) {
const childNode = node.childNodes[j];
const childSearchIndex = childNode.textContent.indexOf(uri);
if (childSearchIndex !== -1) {
// Match found in currentNode
this._replaceNodeSubstringWithNode(childNode, linkElement, uri, childSearchIndex);
// Don't need to count nodesAdded by replacing the node as this
// is a child node, not a top-level node.
break;
}
}
} else {
// Matches part of string in a single text node
const nodesAdded = this._replaceNodeSubstringWithNode(node, linkElement, uri, searchIndex);
// No need to consider the new nodes
i += nodesAdded;
}
result.push(linkElement);
// Find the next match
match = row.textContent.substring(rowStartIndex).match(matcher.regex);
if (!match || match.length === 0) {
return result;
}
uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
rowStartIndex += match.index + uri.length;
}
}
return result;
}
/**
* Creates a link anchor element.
* @param {string} uri The uri of the link.
* @return {HTMLAnchorElement} The link.
*/
private _createAnchorElement(uri: string, handler: LinkMatcherHandler, isHypertextLinkHandler: boolean): HTMLAnchorElement {
const element = this._document.createElement('a');
element.textContent = uri;
element.draggable = false;
if (isHypertextLinkHandler) {
element.href = uri;
// Force link on another tab so work is not lost
element.target = '_blank';
element.addEventListener('click', (event: MouseEvent) => {
if (handler) {
return handler(event, uri);
}
});
} else {
element.addEventListener('click', (event: MouseEvent) => {
// Don't execute the handler if the link is flagged as invalid
if (element.classList.contains(INVALID_LINK_CLASS)) {
return;
}
return handler(event, uri);
});
}
return element;
}
/**
* Replace a node with 1 or more other nodes.
* @param {Node} oldNode The node to replace.
* @param {Node[]} newNodes The new nodes to insert in order.
*/
private _replaceNode(oldNode: Node, ...newNodes: Node[]): void {
const parent = oldNode.parentNode;
for (let i = 0; i < newNodes.length; i++) {
parent.insertBefore(newNodes[i], oldNode);
}
parent.removeChild(oldNode);
}
/**
* Replace a substring within a node with a new node.
* @param {Node} targetNode The target node; either a text node or a <span>
* containing a single text node.
* @param {Node} newNode The new node to insert.
* @param {string} substring The substring to replace.
* @param {number} substringIndex The index of the substring within the string.
* @return The number of nodes to skip when searching for the next uri.
*/
private _replaceNodeSubstringWithNode(targetNode: Node, newNode: Node, substring: string, substringIndex: number): number {
// If the targetNode is a non-text node with a single child, make the child
// the new targetNode.
if (targetNode.childNodes.length === 1) {
targetNode = targetNode.childNodes[0];
}
// The targetNode will be either a text node or a <span>. The text node
// (targetNode or its only-child) needs to be replaced with newNode plus new
// text nodes potentially on either side.
if (targetNode.nodeType !== 3/*Node.TEXT_NODE*/) {
throw new Error('targetNode must be a text node or only contain a single text node');
}
const fullText = targetNode.textContent;
if (substringIndex === 0) {
// Replace with <newNode><textnode>
const rightText = fullText.substring(substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(targetNode, newNode, rightTextNode);
return 0;
}
if (substringIndex === targetNode.textContent.length - substring.length) {
// Replace with <textnode><newNode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
this._replaceNode(targetNode, leftTextNode, newNode);
return 0;
}
// Replace with <textnode><newNode><textnode>
const leftText = fullText.substring(0, substringIndex);
const leftTextNode = this._document.createTextNode(leftText);
const rightText = fullText.substring(substringIndex + substring.length);
const rightTextNode = this._document.createTextNode(rightText);
this._replaceNode(targetNode, leftTextNode, newNode, rightTextNode);
return 1;
}
}
+18 -9
View File
@@ -52,7 +52,7 @@ escapedStateHandler['c'] = (parser, terminal) => {
};
escapedStateHandler['E'] = (parser, terminal) => {
// ESC E Next Line ( NEL is 0x85).
terminal.x = 0;
terminal.buffer.x = 0;
terminal.index();
parser.setState(ParserState.NORMAL);
};
@@ -181,9 +181,13 @@ export class Parser {
*
* @param data The data to parse.
*/
public parse(data: string) {
public parse(data: string): ParserState {
let l = data.length, j, cs, ch, code, low;
if (this._terminal.debug) {
this._terminal.log('data: ' + data);
}
this._position = 0;
// apply leftover surrogate high from last write
if (this._terminal.surrogate_high) {
@@ -458,6 +462,9 @@ export class Parser {
case ParserState.CSI:
if (ch in csiStateHandler) {
if (this._terminal.debug) {
this._terminal.log(`CSI ${this._terminal.prefix ? this._terminal.prefix : ''} ${this._terminal.params ? this._terminal.params.join(';') : ''} ${this._terminal.postfix ? this._terminal.postfix : ''} ${ch}`);
}
csiStateHandler[ch](this._inputHandler, this._terminal.params, this._terminal.prefix, this._terminal.postfix, this);
} else {
this._terminal.error('Unknown CSI code: %s.', ch);
@@ -471,6 +478,8 @@ export class Parser {
case ParserState.DCS:
if (ch === C0.ESC || ch === C0.BEL) {
if (ch === C0.ESC) this._position++;
let pt;
let valid: boolean;
switch (this._terminal.prefix) {
// User-Defined Keys (DECUDK).
@@ -480,8 +489,8 @@ export class Parser {
// Request Status String (DECRQSS).
// test: echo -e '\eP$q"p\e\\'
case '$q':
let pt = this._terminal.currentParam
, valid = false;
pt = this._terminal.currentParam;
valid = false;
switch (pt) {
// DECSCA
@@ -497,9 +506,9 @@ export class Parser {
// DECSTBM
case 'r':
pt = ''
+ (this._terminal.scrollTop + 1)
+ (this._terminal.buffer.scrollTop + 1)
+ ';'
+ (this._terminal.scrollBottom + 1)
+ (this._terminal.buffer.scrollBottom + 1)
+ 'r';
break;
@@ -526,9 +535,8 @@ export class Parser {
// This can cause a small glitch in vim.
// test: echo -ne '\eP+q6b64\e\\'
case '+q':
// TODO: Don't declare pt twice
/*let*/ pt = this._terminal.currentParam
, valid = false;
pt = this._terminal.currentParam;
valid = false;
this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\');
break;
@@ -563,6 +571,7 @@ export class Parser {
break;
}
}
return this._state;
}
/**
+231 -135
View File
@@ -3,6 +3,7 @@
*/
import { ITerminal } from './Interfaces';
import { DomElementObjectPool } from './utils/DomElementObjectPool';
/**
* The maximum number of refresh frames to skip when the write buffer is non-
@@ -30,12 +31,15 @@ export class Renderer {
private _refreshFramesSkipped = 0;
private _refreshAnimationFrame = null;
private _spanElementObjectPool = new DomElementObjectPool('span');
constructor(private _terminal: ITerminal) {
// Figure out whether boldness affects
// the character width of monospace fonts.
if (brokenBold === null) {
brokenBold = checkBoldBroken((<any>this._terminal).document);
brokenBold = checkBoldBroken((<any>this._terminal).element);
}
this._spanElementObjectPool = new DomElementObjectPool('span');
// TODO: Pull more DOM interactions into Renderer.constructor, element for
// example should be owned by Renderer (and also exposed by Terminal due to
@@ -117,9 +121,8 @@ export class Renderer {
* @param {number} end The row to end at (between fromRow and terminal's height terminal - 1)
*/
private _refresh(start: number, end: number): void {
let x, y, i, line, out, ch, ch_width, width, data, attr, bg, fg, flags, row, parent, focused = document.activeElement;
// If this is a big refresh, remove the terminal rows from the DOM for faster calculations
let parent;
if (end - start >= this._terminal.rows / 2) {
parent = this._terminal.element.parentNode;
if (parent) {
@@ -127,8 +130,8 @@ export class Renderer {
}
}
width = this._terminal.cols;
y = start;
let width = this._terminal.cols;
let y = start;
if (end >= this._terminal.rows) {
this._terminal.log('`end` is too large. Most likely a bad CSR.');
@@ -136,148 +139,180 @@ export class Renderer {
}
for (; y <= end; y++) {
row = y + this._terminal.ydisp;
let row = y + this._terminal.buffer.ydisp;
line = this._terminal.lines.get(row);
if (!line || !this._terminal.children[y]) {
// Continue if the line is not available, this means a resize is currently in progress
continue;
}
out = '';
let line = this._terminal.buffer.lines.get(row);
if (this._terminal.y === y - (this._terminal.ybase - this._terminal.ydisp)
&& this._terminal.cursorState
&& !this._terminal.cursorHidden) {
x = this._terminal.x;
let x;
if (this._terminal.buffer.y === y - (this._terminal.buffer.ybase - this._terminal.buffer.ydisp) &&
this._terminal.cursorState &&
!this._terminal.cursorHidden) {
x = this._terminal.buffer.x;
} else {
x = -1;
}
attr = this._terminal.defAttr;
i = 0;
let attr = this._terminal.defAttr;
for (; i < width; i++) {
if (!line[i]) {
// Continue if the character is not available, this means a resize is currently in progress
const documentFragment = document.createDocumentFragment();
let innerHTML = '';
let currentElement;
// Return the row's spans to the pool
while (this._terminal.children[y].children.length) {
const child = this._terminal.children[y].children[0];
this._terminal.children[y].removeChild(child);
this._spanElementObjectPool.release(<HTMLElement>child);
}
for (let i = 0; i < width; i++) {
// TODO: Could data be a more specific type?
let data: any = line[i][0];
const ch = line[i][1];
const ch_width: any = line[i][2];
const isCursor: boolean = i === x;
if (!ch_width) {
continue;
}
data = line[i][0];
ch = line[i][1];
ch_width = line[i][2];
if (!ch_width)
continue;
if (i === x) data = -1;
if (data !== attr) {
if (attr !== this._terminal.defAttr) {
out += '</span>';
}
if (data !== this._terminal.defAttr) {
if (data === -1) {
out += '<span class="reverse-video terminal-cursor">';
} else {
let classNames = [];
bg = data & 0x1ff;
fg = (data >> 9) & 0x1ff;
flags = data >> 18;
if (flags & FLAGS.BOLD) {
if (!brokenBold) {
classNames.push('xterm-bold');
}
// See: XTerm*boldColors
if (fg < 8) fg += 8;
}
if (flags & FLAGS.UNDERLINE) {
classNames.push('xterm-underline');
}
if (flags & FLAGS.BLINK) {
classNames.push('xterm-blink');
}
// If inverse flag is on, then swap the foreground and background variables.
if (flags & FLAGS.INVERSE) {
/* One-line variable swap in JavaScript: http://stackoverflow.com/a/16201730 */
bg = [fg, fg = bg][0];
// Should inverse just be before the
// above boldColors effect instead?
if ((flags & 1) && fg < 8) fg += 8;
}
if (flags & FLAGS.INVISIBLE) {
classNames.push('xterm-hidden');
}
/**
* Weird situation: Invert flag used black foreground and white background results
* in invalid background color, positioned at the 256 index of the 256 terminal
* color map. Pin the colors manually in such a case.
*
* Source: https://github.com/sourcelair/xterm.js/issues/57
*/
if (flags & FLAGS.INVERSE) {
if (bg === 257) {
bg = 15;
}
if (fg === 256) {
fg = 0;
}
}
if (bg < 256) {
classNames.push('xterm-bg-color-' + bg);
}
if (fg < 256) {
classNames.push('xterm-color-' + fg);
}
out += '<span';
if (classNames.length) {
out += ' class="' + classNames.join(' ') + '"';
}
out += '>';
if (data !== attr || isCursor) {
if (attr !== this._terminal.defAttr && !isCursor) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
if (data !== this._terminal.defAttr || isCursor) {
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
}
currentElement = this._spanElementObjectPool.acquire();
let bg = data & 0x1ff;
let fg = (data >> 9) & 0x1ff;
let flags = data >> 18;
if (isCursor) {
currentElement.classList.add('reverse-video');
currentElement.classList.add('terminal-cursor');
}
if (flags & FLAGS.BOLD) {
if (!brokenBold) {
currentElement.classList.add('xterm-bold');
}
// See: XTerm*boldColors
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
currentElement.classList.add('xterm-underline');
}
if (flags & FLAGS.BLINK) {
currentElement.classList.add('xterm-blink');
}
// If inverse flag is on, then swap the foreground and background variables.
if (flags & FLAGS.INVERSE) {
let temp = bg;
bg = fg;
fg = temp;
// Should inverse just be before the above boldColors effect instead?
if ((flags & 1) && fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.INVISIBLE && !isCursor) {
currentElement.classList.add('xterm-hidden');
}
/**
* Weird situation: Invert flag used black foreground and white background results
* in invalid background color, positioned at the 256 index of the 256 terminal
* color map. Pin the colors manually in such a case.
*
* Source: https://github.com/sourcelair/xterm.js/issues/57
*/
if (flags & FLAGS.INVERSE) {
if (bg === 257) {
bg = 15;
}
if (fg === 256) {
fg = 0;
}
}
if (bg < 256) {
currentElement.classList.add(`xterm-bg-color-${bg}`);
}
if (fg < 256) {
currentElement.classList.add(`xterm-color-${fg}`);
}
}
}
if (ch_width === 2) {
out += '<span class="xterm-wide-char">';
}
switch (ch) {
case '&':
out += '&amp;';
break;
case '<':
out += '&lt;';
break;
case '>':
out += '&gt;';
break;
default:
if (ch <= ' ') {
out += '&nbsp;';
} else {
out += ch;
}
break;
}
if (ch_width === 2) {
out += '</span>';
// Wrap wide characters so they're sized correctly. It's more difficult to release these
// from the object pool so just create new ones via innerHTML.
innerHTML += `<span class="xterm-wide-char">${ch}</span>`;
} else if (ch.charCodeAt(0) > 255) {
// Wrap any non-wide unicode character as some fonts size them badly
innerHTML += `<span class="xterm-normal-char">${ch}</span>`;
} else {
switch (ch) {
case '&':
innerHTML += '&amp;';
break;
case '<':
innerHTML += '&lt;';
break;
case '>':
innerHTML += '&gt;';
break;
default:
if (ch <= ' ') {
innerHTML += '&nbsp;';
} else {
innerHTML += ch;
}
break;
}
}
attr = data;
// The cursor needs its own element, therefore we set attr to -1
// which will cause the next character to be rendered in a new element
attr = isCursor ? -1 : data;
}
if (attr !== this._terminal.defAttr) {
out += '</span>';
if (innerHTML && !currentElement) {
currentElement = this._spanElementObjectPool.acquire();
}
if (currentElement) {
if (innerHTML) {
currentElement.innerHTML = innerHTML;
innerHTML = '';
}
documentFragment.appendChild(currentElement);
currentElement = null;
}
this._terminal.children[y].innerHTML = out;
this._terminal.children[y].appendChild(documentFragment);
}
if (parent) {
@@ -286,19 +321,80 @@ export class Renderer {
this._terminal.emit('refresh', {element: this._terminal.element, start: start, end: end});
};
/**
* Refreshes the selection in the DOM.
* @param start The selection start.
* @param end The selection end.
*/
public refreshSelection(start: [number, number], end: [number, number]) {
// Remove all selections
while (this._terminal.selectionContainer.children.length) {
this._terminal.selectionContainer.removeChild(this._terminal.selectionContainer.children[0]);
}
// Selection does not exist
if (!start || !end) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = document.createDocumentFragment();
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
this._terminal.selectionContainer.appendChild(documentFragment);
}
/**
* Creates a selection element at the specified position.
* @param row The row of the selection.
* @param colStart The start column.
* @param colEnd The end columns.
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
return element;
}
}
// if bold is broken, we can't
// use it in the terminal.
function checkBoldBroken(document) {
const body = document.getElementsByTagName('body')[0];
// If bold is broken, we can't use it in the terminal.
function checkBoldBroken(terminal) {
const document = terminal.ownerDocument;
const el = document.createElement('span');
el.innerHTML = 'hello world';
body.appendChild(el);
const w1 = el.scrollWidth;
terminal.appendChild(el);
const w1 = el.offsetWidth;
const h1 = el.offsetHeight;
el.style.fontWeight = 'bold';
const w2 = el.scrollWidth;
body.removeChild(el);
return w1 !== w2;
const w2 = el.offsetWidth;
const h2 = el.offsetHeight;
terminal.removeChild(el);
return w1 !== w2 || h1 !== h2;
}
+224
View File
@@ -0,0 +1,224 @@
/**
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ICircularList } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: ICircularList<[number, string, number][]>,
rowContainer: HTMLElement,
charMeasure: CharMeasure
) {
super(terminal, buffer, rowContainer, charMeasure);
}
public get model(): SelectionModel { return this._model; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
// Disable DOM interaction
public enable(): void {}
public disable(): void {}
public refresh(): void {}
}
describe('SelectionManager', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let terminal: ITerminal;
let bufferLines: ICircularList<[number, string, number][]>;
let rowContainer: HTMLElement;
let selectionManager: TestSelectionManager;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
rowContainer = document.createElement('div');
terminal = <any>{ cols: 80, rows: 2 };
terminal.scrollback = 100;
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
bufferLines = terminal.buffer.lines;
selectionManager = new TestSelectionManager(terminal, bufferLines, rowContainer, null);
});
function stringToRow(text: string): [number, string, number][] {
let result: [number, string, number][] = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1]);
}
return result;
}
describe('_selectWordAt', () => {
it('should expand selection for normal width chars', () => {
bufferLines.push(stringToRow('foo bar'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, 'bar');
selectionManager.selectWordAt([5, 0]);
assert.equal(selectionManager.selectionText, 'bar');
selectionManager.selectWordAt([6, 0]);
assert.equal(selectionManager.selectionText, 'bar');
});
it('should expand selection for whitespace', () => {
bufferLines.push(stringToRow('a b'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, 'a');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, ' ');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, 'b');
});
it('should expand selection for wide characters', () => {
// Wide characters use a special format
bufferLines.push([
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, ' ', 1],
[null, 'a', 1],
[null, '中', 2],
[null, '', 0],
[null, '文', 2],
[null, '', 0],
[null, 'b', 1],
[null, ' ', 1],
[null, 'f', 1],
[null, 'o', 1],
[null, 'o', 1]
]);
// Ensure wide characters take up 2 columns
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, '中文');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, ' ');
// Ensure wide characters work when wrapped in normal width characters
selectionManager.selectWordAt([5, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([6, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([7, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([8, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([9, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([10, 0]);
assert.equal(selectionManager.selectionText, 'a中文b');
selectionManager.selectWordAt([11, 0]);
assert.equal(selectionManager.selectionText, ' ');
// Ensure normal width characters work fine in a line containing wide characters
selectionManager.selectWordAt([12, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([13, 0]);
assert.equal(selectionManager.selectionText, 'foo');
selectionManager.selectWordAt([14, 0]);
assert.equal(selectionManager.selectionText, 'foo');
});
it('should select up to non-path characters that are commonly adjacent to paths', () => {
bufferLines.push(stringToRow('(cd)[ef]{gh}\'ij"'));
selectionManager.selectWordAt([0, 0]);
assert.equal(selectionManager.selectionText, '(cd');
selectionManager.selectWordAt([1, 0]);
assert.equal(selectionManager.selectionText, 'cd');
selectionManager.selectWordAt([2, 0]);
assert.equal(selectionManager.selectionText, 'cd');
selectionManager.selectWordAt([3, 0]);
assert.equal(selectionManager.selectionText, 'cd)');
selectionManager.selectWordAt([4, 0]);
assert.equal(selectionManager.selectionText, '[ef');
selectionManager.selectWordAt([5, 0]);
assert.equal(selectionManager.selectionText, 'ef');
selectionManager.selectWordAt([6, 0]);
assert.equal(selectionManager.selectionText, 'ef');
selectionManager.selectWordAt([7, 0]);
assert.equal(selectionManager.selectionText, 'ef]');
selectionManager.selectWordAt([8, 0]);
assert.equal(selectionManager.selectionText, '{gh');
selectionManager.selectWordAt([9, 0]);
assert.equal(selectionManager.selectionText, 'gh');
selectionManager.selectWordAt([10, 0]);
assert.equal(selectionManager.selectionText, 'gh');
selectionManager.selectWordAt([11, 0]);
assert.equal(selectionManager.selectionText, 'gh}');
selectionManager.selectWordAt([12, 0]);
assert.equal(selectionManager.selectionText, '\'ij');
selectionManager.selectWordAt([13, 0]);
assert.equal(selectionManager.selectionText, 'ij');
selectionManager.selectWordAt([14, 0]);
assert.equal(selectionManager.selectionText, 'ij');
selectionManager.selectWordAt([15, 0]);
assert.equal(selectionManager.selectionText, 'ij"');
});
});
describe('_selectLineAt', () => {
it('should select the entire line', () => {
bufferLines.push(stringToRow('foo bar'));
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foo bar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
});
});
describe('selectAll', () => {
it('should select the entire buffer, beyond the viewport', () => {
bufferLines.push(stringToRow('1'));
bufferLines.push(stringToRow('2'));
bufferLines.push(stringToRow('3'));
bufferLines.push(stringToRow('4'));
bufferLines.push(stringToRow('5'));
selectionManager.selectAll();
terminal.buffer.ybase = bufferLines.length - terminal.rows;
assert.equal(selectionManager.selectionText, '1\n2\n3\n4\n5');
});
});
describe('hasSelection', () => {
it('should return whether there is a selection', () => {
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionStartLength = 0;
assert.equal(selectionManager.hasSelection, false);
selectionManager.model.selectionEnd = [0, 0];
assert.equal(selectionManager.hasSelection, false);
selectionManager.model.selectionEnd = [1, 0];
assert.equal(selectionManager.hasSelection, true);
selectionManager.model.selectionEnd = [0, 1];
assert.equal(selectionManager.hasSelection, true);
selectionManager.model.selectionEnd = [1, 1];
assert.equal(selectionManager.hasSelection, true);
});
});
});
+671
View File
@@ -0,0 +1,671 @@
/**
* @license MIT
*/
import * as Mouse from './utils/Mouse';
import * as Browser from './utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal, ICircularList } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import { translateBufferLineToString } from './utils/BufferLine';
/**
* The number of pixels the mouse needs to be above or below the viewport in
* order to scroll at the maximum speed.
*/
const DRAG_SCROLL_MAX_THRESHOLD = 50;
/**
* The maximum scrolling speed
*/
const DRAG_SCROLL_MAX_SPEED = 15;
/**
* The number of milliseconds between drag scroll updates.
*/
const DRAG_SCROLL_INTERVAL = 50;
/**
* A string containing all characters that are considered word separated by the
* double click to select work logic.
*/
const WORD_SEPARATORS = ' ()[]{}\'"';
// TODO: Move these constants elsewhere, they belong in a buffer or buffer
// data/line class.
const LINE_DATA_CHAR_INDEX = 1;
const LINE_DATA_WIDTH_INDEX = 2;
const NON_BREAKING_SPACE_CHAR = String.fromCharCode(160);
const ALL_NON_BREAKING_SPACE_REGEX = new RegExp(NON_BREAKING_SPACE_CHAR, 'g');
/**
* Represents a position of a word on a line.
*/
interface IWordPosition {
start: number;
length: number;
}
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
enum SelectionMode {
NORMAL,
WORD,
LINE
}
/**
* A class that manages the selection of the terminal. With help from
* SelectionModel, SelectionManager handles with all logic associated with
* dealing with the selection, including handling mouse interaction, wide
* characters and fetching the actual text within the selection. Rendering is
* not handled by the SelectionManager but a 'refresh' event is fired when the
* selection is ready to be redrawn.
*/
export class SelectionManager extends EventEmitter {
protected _model: SelectionModel;
/**
* The amount to scroll every drag scroll update (depends on how far the mouse
* drag is above or below the terminal).
*/
private _dragScrollAmount: number;
/**
* The current selection mode.
*/
private _activeSelectionMode: SelectionMode;
/**
* A setInterval timer that is active while the mouse is down whose callback
* scrolls the viewport when necessary.
*/
private _dragScrollIntervalTimer: NodeJS.Timer;
/**
* The animation frame ID used for refreshing the selection.
*/
private _refreshAnimationFrame: number;
/**
* Whether selection is enabled.
*/
private _enabled = true;
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
constructor(
private _terminal: ITerminal,
private _buffer: ICircularList<[number, string, number][]>,
private _rowContainer: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
this._initListeners();
this.enable();
this._model = new SelectionModel(_terminal);
this._activeSelectionMode = SelectionMode.NORMAL;
}
/**
* Initializes listener variables.
*/
private _initListeners() {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._rowContainer.addEventListener('mousedown', event => this._onMouseDown(<MouseEvent>event));
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
this._buffer.on('trim', (amount: number) => this._onTrim(amount));
}
/**
* Disables the selection manager. This is useful for when terminal mouse
* are enabled.
*/
public disable() {
this.clearSelection();
this._enabled = false;
}
/**
* Enable the selection manager.
*/
public enable() {
this._enabled = true;
}
/**
* Sets the active buffer, this should be called when the alt buffer is
* switched in or out.
* @param buffer The active buffer.
*/
public setBuffer(buffer: ICircularList<[number, string, number][]>): void {
this._buffer = buffer;
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
/**
* Gets whether there is an active text selection.
*/
public get hasSelection(): boolean {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return start[0] !== end[0] || start[1] !== end[1];
}
/**
* Gets the text currently selected.
*/
public get selectionText(): string {
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return '';
}
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
let result: string[] = [];
result.push(translateBufferLineToString(this._buffer.get(start[1]), true, start[0], startRowEndCol));
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.get(i);
const lineText = translateBufferLineToString(bufferLine, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.get(end[1]);
const lineText = translateBufferLineToString(bufferLine, true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// Format string by replacing non-breaking space chars with regular spaces
// and joining the array into a multi-line string.
const formattedResult = result.map(line => {
return line.replace(ALL_NON_BREAKING_SPACE_REGEX, ' ');
}).join(Browser.isMSWindows ? '\r\n' : '\n');
return formattedResult;
}
/**
* Clears the current terminal selection.
*/
public clearSelection(): void {
this._model.clearSelection();
this._removeMouseDownListeners();
this.refresh();
}
/**
* Queues a refresh, redrawing the selection on the next opportunity.
* @param isNewSelection Whether the selection should be registered as a new
* selection on Linux.
*/
public refresh(isNewSelection?: boolean): void {
// Queue the refresh for the renderer
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
}
// If the platform is Linux and the refresh call comes from a mouse event,
// we need to update the selection for middle click to paste selection.
if (Browser.isLinux && isNewSelection) {
const selectionText = this.selectionText;
if (selectionText.length) {
this.emit('newselection', this.selectionText);
}
}
}
/**
* Fires the refresh event, causing consumers to pick it up and redraw the
* selection state.
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
}
/**
* Selects all text within the terminal.
*/
public selectAll(): void {
this._model.isSelectAllActive = true;
this.refresh();
}
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
*/
private _onTrim(amount: number) {
const needsRefresh = this._model.onTrim(amount);
if (needsRefresh) {
this.refresh();
}
}
/**
* Gets the 0-based [x, y] buffer coordinates of the current mouse event.
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = Mouse.getCoords(event, this._rowContainer, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
// Convert to 0-based
coords[0]--;
coords[1]--;
// Convert viewport coords to buffer coords
coords[1] += this._terminal.buffer.ydisp;
return coords;
}
/**
* Gets the amount the viewport should be scrolled based on how far out of the
* terminal the mouse is.
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = Mouse.getCoordsRelativeToElement(event, this._rowContainer)[1];
const terminalHeight = this._terminal.rows * this._charMeasure.height;
if (offset >= 0 && offset <= terminalHeight) {
return 0;
}
if (offset > terminalHeight) {
offset -= terminalHeight;
}
offset = Math.min(Math.max(offset, -DRAG_SCROLL_MAX_THRESHOLD), DRAG_SCROLL_MAX_THRESHOLD);
offset /= DRAG_SCROLL_MAX_THRESHOLD;
return (offset / Math.abs(offset)) + Math.round(offset * (DRAG_SCROLL_MAX_SPEED - 1));
}
/**
* Handles te mousedown event, setting up for a new selection.
* @param event The mousedown event.
*/
private _onMouseDown(event: MouseEvent) {
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
if (event.button === 2 && this.hasSelection) {
event.stopPropagation();
return;
}
// Only action the primary button
if (event.button !== 0) {
return;
}
// Allow selection when using a specific modifier key, even when disabled
if (!this._enabled) {
const shouldForceSelection = Browser.isMac && event.altKey;
if (!shouldForceSelection) {
return;
}
// Don't send the mouse down event to the current process, we want to select
event.stopPropagation();
}
// Tell the browser not to start a regular selection
event.preventDefault();
// Reset drag scroll state
this._dragScrollAmount = 0;
if (this._enabled && event.shiftKey) {
this._onIncrementalClick(event);
} else {
if (event.detail === 1) {
this._onSingleClick(event);
} else if (event.detail === 2) {
this._onDoubleClick(event);
} else if (event.detail === 3) {
this._onTripleClick(event);
}
}
this._addMouseDownListeners();
this.refresh(true);
}
/**
* Adds listeners when mousedown is triggered.
*/
private _addMouseDownListeners(): void {
// Listen on the document so that dragging outside of viewport works
this._rowContainer.ownerDocument.addEventListener('mousemove', this._mouseMoveListener);
this._rowContainer.ownerDocument.addEventListener('mouseup', this._mouseUpListener);
this._dragScrollIntervalTimer = setInterval(() => this._dragScroll(), DRAG_SCROLL_INTERVAL);
}
/**
* Removes the listeners that are registered when mousedown is triggered.
*/
private _removeMouseDownListeners(): void {
this._rowContainer.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
this._rowContainer.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
clearInterval(this._dragScrollIntervalTimer);
this._dragScrollIntervalTimer = null;
}
/**
* Performs an incremental click, setting the selection end position to the mouse
* position.
* @param event The mouse event.
*/
private _onIncrementalClick(event: MouseEvent): void {
if (this._model.selectionStart) {
this._model.selectionEnd = this._getMouseBufferCoords(event);
}
}
/**
* Performs a single click, resetting relevant state and setting the selection
* start position.
* @param event The mouse event.
*/
private _onSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = SelectionMode.NORMAL;
// Initialize the new selection
this._model.selectionStart = this._getMouseBufferCoords(event);
if (!this._model.selectionStart) {
return;
}
this._model.selectionEnd = null;
// Ensure the line exists
const line = this._buffer.get(this._model.selectionStart[1]);
if (!line) {
return;
}
// If the mouse is over the second half of a wide character, adjust the
// selection to cover the whole character
const char = line[this._model.selectionStart[0]];
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
this._model.selectionStart[0]++;
}
}
/**
* Performs a double click, selecting the current work.
* @param event The mouse event.
*/
private _onDoubleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords);
}
}
/**
* Performs a triple click, selecting the current line and activating line
* select mode.
* @param event The mouse event.
*/
private _onTripleClick(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.LINE;
this._selectLineAt(coords[1]);
}
}
/**
* Handles the mousemove event when the mouse button is down, recording the
* end of the selection and refreshing the selection.
* @param event The mousemove event.
*/
private _onMouseMove(event: MouseEvent) {
// Record the previous position so we know whether to redraw the selection
// at the end.
const previousSelectionEnd = this._model.selectionEnd ? [this._model.selectionEnd[0], this._model.selectionEnd[1]] : null;
// Set the initial selection end based on the mouse coordinates
this._model.selectionEnd = this._getMouseBufferCoords(event);
if (!this._model.selectionEnd) {
this.refresh(true);
return;
}
// Select the entire line if line select mode is active.
if (this._activeSelectionMode === SelectionMode.LINE) {
if (this._model.selectionEnd[1] < this._model.selectionStart[1]) {
this._model.selectionEnd[0] = 0;
} else {
this._model.selectionEnd[0] = this._terminal.cols;
}
} else if (this._activeSelectionMode === SelectionMode.WORD) {
this._selectToWordAt(this._model.selectionEnd);
}
// Determine the amount of scrolling that will happen.
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
// If the cursor was above or below the viewport, make sure it's at the
// start or end of the viewport respectively.
if (this._dragScrollAmount > 0) {
this._model.selectionEnd[0] = this._terminal.cols - 1;
} else if (this._dragScrollAmount < 0) {
this._model.selectionEnd[0] = 0;
}
// If the character is a wide character include the cell to the right in the
// selection. Note that selections at the very end of the line will never
// have a character.
if (this._model.selectionEnd[1] < this._buffer.length) {
const char = this._buffer.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
if (char && char[2] === 0) {
this._model.selectionEnd[0]++;
}
}
// Only draw here if the selection changes.
if (!previousSelectionEnd ||
previousSelectionEnd[0] !== this._model.selectionEnd[0] ||
previousSelectionEnd[1] !== this._model.selectionEnd[1]) {
this.refresh(true);
}
}
/**
* The callback that occurs every DRAG_SCROLL_INTERVAL ms that does the
* scrolling of the viewport.
*/
private _dragScroll() {
if (this._dragScrollAmount) {
this._terminal.scrollDisp(this._dragScrollAmount, false);
// Re-evaluate selection
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
} else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
this.refresh();
}
}
/**
* Handles the mouseup event, removing the mousedown listeners.
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent) {
this._removeMouseDownListeners();
}
/**
* Converts a viewport column to the character index on the buffer line, the
* latter takes into account wide characters.
* @param coords The coordinates to find the 2 index for.
*/
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
let charIndex = coords[0];
for (let i = 0; coords[0] >= i; i++) {
const char = bufferLine[i];
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
charIndex--;
}
}
return charIndex;
}
public setSelection(col: number, row: number, length: number): void {
this._model.clearSelection();
this._removeMouseDownListeners();
this._model.selectionStart = [col, row];
this._model.selectionStartLength = length;
this.refresh();
}
/**
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number]): IWordPosition {
const bufferLine = this._buffer.get(coords[1]);
if (!bufferLine) {
return null;
}
const line = translateBufferLineToString(bufferLine, false);
// Get actual index, taking into consideration wide characters
let endIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
let startIndex = endIndex;
// Record offset to be used later
const charOffset = coords[0] - startIndex;
let leftWideCharCount = 0;
let rightWideCharCount = 0;
if (line.charAt(startIndex) === ' ') {
// Expand until non-whitespace is hit
while (startIndex > 0 && line.charAt(startIndex - 1) === ' ') {
startIndex--;
}
while (endIndex < line.length && line.charAt(endIndex + 1) === ' ') {
endIndex++;
}
} else {
// Expand until whitespace is hit. This algorithm works by scanning left
// and right from the starting position, keeping both the index format
// (line) and the column format (bufferLine) in sync. When a wide
// character is hit, it is recorded and the column index is adjusted.
let startCol = coords[0];
let endCol = coords[0];
// Consider the initial position, skip it and increment the wide char
// variable
if (bufferLine[startCol][LINE_DATA_WIDTH_INDEX] === 0) {
leftWideCharCount++;
startCol--;
}
if (bufferLine[endCol][LINE_DATA_WIDTH_INDEX] === 2) {
rightWideCharCount++;
endCol++;
}
// Expand the string in both directions until a space is hit
while (startIndex > 0 && !this._isCharWordSeparator(line.charAt(startIndex - 1))) {
if (bufferLine[startCol - 1][LINE_DATA_WIDTH_INDEX] === 0) {
// If the next character is a wide char, record it and skip the column
leftWideCharCount++;
startCol--;
}
startIndex--;
startCol--;
}
while (endIndex + 1 < line.length && !this._isCharWordSeparator(line.charAt(endIndex + 1))) {
if (bufferLine[endCol + 1][LINE_DATA_WIDTH_INDEX] === 2) {
// If the next character is a wide char, record it and skip the column
rightWideCharCount++;
endCol++;
}
endIndex++;
endCol++;
}
}
const start = startIndex + charOffset - leftWideCharCount;
const length = Math.min(endIndex - startIndex + leftWideCharCount + rightWideCharCount + 1/*include endIndex char*/, this._terminal.cols);
return { start, length };
}
/**
* Selects the word at the coordinates specified.
* @param coords The coordinates to get the word at.
*/
protected _selectWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
if (wordPosition) {
this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length;
}
}
/**
* Sets the selection end to the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
}
}
/**
* Gets whether the character is considered a word separator by the select
* word logic.
* @param char The character to check.
*/
private _isCharWordSeparator(char: string): boolean {
return WORD_SEPARATORS.indexOf(char) >= 0;
}
/**
* Selects the line specified.
* @param line The line index.
*/
protected _selectLineAt(line: number): void {
this._model.selectionStart = [0, line];
this._model.selectionStartLength = this._terminal.cols;
}
}
+136
View File
@@ -0,0 +1,136 @@
/**
* @license MIT
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet';
class TestSelectionModel extends SelectionModel {
constructor(
terminal: ITerminal
) {
super(terminal);
}
}
describe('SelectionManager', () => {
let window: Window;
let document: Document;
let terminal: ITerminal;
let model: TestSelectionModel;
beforeEach(() => {
terminal = <any>{ cols: 80, rows: 2, ybase: 0 };
terminal.scrollback = 10;
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
model = new TestSelectionModel(terminal);
});
describe('clearSelection', () => {
it('should clear the final selection', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 2]);
model.clearSelection();
assert.deepEqual(model.finalSelectionStart, null);
assert.deepEqual(model.finalSelectionEnd, null);
});
});
describe('areSelectionValuesReversed', () => {
it('should return true when the selection end is before selection start', () => {
model.selectionStart = [1, 0];
model.selectionEnd = [0, 0];
assert.equal(model.areSelectionValuesReversed(), true);
model.selectionStart = [10, 2];
model.selectionEnd = [0, 0];
assert.equal(model.areSelectionValuesReversed(), true);
});
it('should return false when the selection end is after selection start', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [1, 0];
assert.equal(model.areSelectionValuesReversed(), false);
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
assert.equal(model.areSelectionValuesReversed(), false);
});
});
describe('onTrim', () => {
it('should trim a portion of the selection when a part of it is trimmed', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 2];
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
});
it('should clear selection when it is trimmed in its entirety', () => {
model.selectionStart = [0, 0];
model.selectionEnd = [10, 0];
model.onTrim(1);
assert.deepEqual(model.finalSelectionStart, null);
assert.deepEqual(model.finalSelectionEnd, null);
});
});
describe('finalSelectionStart', () => {
it('should return the start of the buffer if select all is active', () => {
model.isSelectAllActive = true;
assert.deepEqual(model.finalSelectionStart, [0, 0]);
});
it('should return selection start if there is no selection end', () => {
model.selectionStart = [2, 2];
assert.deepEqual(model.finalSelectionStart, [2, 2]);
});
it('should return selection end if values are reversed', () => {
model.selectionStart = [2, 2];
model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionStart, [2, 2]);
model.selectionEnd = [1, 2];
assert.deepEqual(model.finalSelectionStart, [1, 2]);
});
});
describe('finalSelectionEnd', () => {
it('should return the end of the buffer if select all is active', () => {
model.isSelectAllActive = true;
assert.deepEqual(model.finalSelectionEnd, [80, 1]);
});
it('should return null if there is no selection start', () => {
assert.equal(model.finalSelectionEnd, null);
model.selectionEnd = [1, 2];
assert.equal(model.finalSelectionEnd, null);
});
it('should return selection start + length if there is no selection end', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection start + length if values are reversed', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [2, 1];
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection start + length if selection end is inside the start selection', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [3, 2];
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
});
it('should return selection end if selection end is after selection start + length', () => {
model.selectionStart = [2, 2];
model.selectionStartLength = 2;
model.selectionEnd = [5, 2];
assert.deepEqual(model.finalSelectionEnd, [5, 2]);
});
});
});
+128
View File
@@ -0,0 +1,128 @@
/**
* @license MIT
*/
import { ITerminal } from './Interfaces';
/**
* Represents a selection within the buffer. This model only cares about column
* and row coordinates, not wide characters.
*/
export class SelectionModel {
/**
* Whether select all is currently active.
*/
public isSelectAllActive: boolean;
/**
* The [x, y] position the selection starts at.
*/
public selectionStart: [number, number];
/**
* The minimal length of the selection from the start position. When double
* clicking on a word, the word will be selected which makes the selection
* start at the start of the word and makes this variable the length.
*/
public selectionStartLength: number;
/**
* The [x, y] position the selection ends at.
*/
public selectionEnd: [number, number];
constructor(
private _terminal: ITerminal
) {
this.clearSelection();
}
/**
* Clears the current selection.
*/
public clearSelection(): void {
this.selectionStart = null;
this.selectionEnd = null;
this.isSelectAllActive = false;
this.selectionStartLength = 0;
}
/**
* The final selection start, taking into consideration select all.
*/
public get finalSelectionStart(): [number, number] {
if (this.isSelectAllActive) {
return [0, 0];
}
if (!this.selectionEnd || !this.selectionStart) {
return this.selectionStart;
}
return this.areSelectionValuesReversed() ? this.selectionEnd : this.selectionStart;
}
/**
* The final selection end, taking into consideration select all, double click
* word selection and triple click line selection.
*/
public get finalSelectionEnd(): [number, number] {
if (this.isSelectAllActive) {
return [this._terminal.cols, this._terminal.buffer.ybase + this._terminal.rows - 1];
}
if (!this.selectionStart) {
return null;
}
// Use the selection start if the end doesn't exist or they're reversed
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
}
// Ensure the the word/line is selected after a double/triple click
if (this.selectionStartLength) {
// Select the larger of the two when start and end are on the same line
if (this.selectionEnd[1] === this.selectionStart[1]) {
return [Math.max(this.selectionStart[0] + this.selectionStartLength, this.selectionEnd[0]), this.selectionEnd[1]];
}
}
return this.selectionEnd;
}
/**
* Returns whether the selection start and end are reversed.
*/
public areSelectionValuesReversed(): boolean {
const start = this.selectionStart;
const end = this.selectionEnd;
return start[1] > end[1] || (start[1] === end[1] && start[0] > end[0]);
}
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
* @return Whether a refresh is necessary.
*/
public onTrim(amount: number): boolean {
// Adjust the selection position based on the trimmed amount.
if (this.selectionStart) {
this.selectionStart[1] -= amount;
}
if (this.selectionEnd) {
this.selectionEnd[1] -= amount;
}
// The selection has moved off the buffer, clear it.
if (this.selectionEnd && this.selectionEnd[1] < 0) {
this.clearSelection();
return true;
}
// If the selection start is trimmed, ensure the start column is 0.
if (this.selectionStart && this.selectionStart[1] < 0) {
this.selectionStart[1] = 0;
}
return false;
}
}
+14
View File
@@ -0,0 +1,14 @@
/**
* @license MIT
*/
export type LinkMatcher = {
id: number,
regex: RegExp,
handler: LinkMatcherHandler,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
+40 -24
View File
@@ -1,5 +1,6 @@
import { assert } from 'chai';
import { Viewport } from './Viewport';
import {BufferSet} from './BufferSet';
describe('Viewport', () => {
let terminal;
@@ -12,7 +13,6 @@ describe('Viewport', () => {
beforeEach(() => {
terminal = {
lines: [],
rows: 0,
ydisp: 0,
on: () => {},
@@ -20,8 +20,16 @@ describe('Viewport', () => {
style: {
lineHeight: 0
}
}
},
selectionContainer: {
style: {
height: 0
}
},
scrollback: 10
};
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
viewportElement = {
addEventListener: () => {},
style: {
@@ -41,38 +49,46 @@ describe('Viewport', () => {
});
describe('refresh', () => {
it('should set the line-height of the terminal', () => {
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
charMeasure.height = 1;
viewport.refresh();
assert.equal(viewportElement.style.lineHeight, '1px');
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
it('should set the line-height of the terminal', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
assert.equal(viewportElement.style.lineHeight, CHARACTER_HEIGHT + 'px');
assert.equal(terminal.rowContainer.style.lineHeight, CHARACTER_HEIGHT + 'px');
charMeasure.height = 1;
viewport.refresh();
assert.equal(viewportElement.style.lineHeight, '1px');
assert.equal(terminal.rowContainer.style.lineHeight, '1px');
done();
}, 0);
});
it('should set the height of the viewport when the line-height changed', () => {
terminal.lines.push('');
terminal.lines.push('');
terminal.buffer.lines.push('');
terminal.buffer.lines.push('');
terminal.rows = 1;
viewport.refresh();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
charMeasure.height = 20;
charMeasure.height = 2 * CHARACTER_HEIGHT;
viewport.refresh();
assert.equal(viewportElement.style.height, 20 + 'px');
assert.equal(viewportElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
});
});
describe('syncScrollArea', () => {
it('should sync the scroll area', () => {
terminal.lines.push('');
terminal.rows = 1;
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
terminal.lines.push('');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
it('should sync the scroll area', done => {
// Allow CharMeasure to be initialized
setTimeout(() => {
terminal.buffer.lines.push('');
terminal.rows = 1;
assert.equal(scrollAreaElement.style.height, 0 * CHARACTER_HEIGHT + 'px');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
terminal.buffer.lines.push('');
viewport.syncScrollArea();
assert.equal(viewportElement.style.height, 1 * CHARACTER_HEIGHT + 'px');
assert.equal(scrollAreaElement.style.height, 2 * CHARACTER_HEIGHT + 'px');
done();
}, 0);
});
});
});
+31 -8
View File
@@ -13,13 +13,14 @@ export class Viewport {
private currentRowHeight: number;
private lastRecordedBufferLength: number;
private lastRecordedViewportHeight: number;
private lastTouchY: number;
/**
* Creates a new Viewport.
* @param terminal The terminal this viewport belongs to.
* @param viewportElement The DOM element acting as the viewport.
* @param scrollArea The DOM element acting as the scroll area.
* @param charMeasureElement A DOM element used to measure the character size of. the terminal.
* @param charMeasure A DOM element used to measure the character size of. the terminal.
*/
constructor(
private terminal: ITerminal,
@@ -35,14 +36,13 @@ export class Viewport {
this.terminal.on('resize', this.syncScrollArea.bind(this));
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
this.syncScrollArea();
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
* @param charSize A character size measurement bounding rect object, if it doesn't exist it will
* be created.
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
@@ -56,6 +56,7 @@ export class Viewport {
if (rowHeightChanged || viewportHeightChanged) {
this.lastRecordedViewportHeight = this.terminal.rows;
this.viewportElement.style.height = this.charMeasure.height * this.terminal.rows + 'px';
this.terminal.selectionContainer.style.height = this.viewportElement.style.height;
}
this.scrollArea.style.height = (this.charMeasure.height * this.lastRecordedBufferLength) + 'px';
}
@@ -65,9 +66,9 @@ export class Viewport {
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
if (this.lastRecordedBufferLength !== this.terminal.lines.length) {
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.lines.length;
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== this.terminal.rows) {
// If viewport height changed
@@ -80,7 +81,7 @@ export class Viewport {
}
// Sync scrollTop
const scrollTop = this.terminal.ydisp * this.currentRowHeight;
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
if (this.viewportElement.scrollTop !== scrollTop) {
this.viewportElement.scrollTop = scrollTop;
}
@@ -93,7 +94,7 @@ export class Viewport {
*/
private onScroll(ev: Event) {
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.ydisp;
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollDisp(diff, true);
}
@@ -119,4 +120,26 @@ export class Viewport {
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
};
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
public onTouchStart(ev: TouchEvent) {
this.lastTouchY = ev.touches[0].pageY;
};
/**
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent) {
let deltaY = this.lastTouchY - ev.touches[0].pageY;
this.lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return;
}
this.viewportElement.scrollTop += deltaY;
ev.preventDefault();
};
}
+7 -2
View File
@@ -32,6 +32,9 @@
var exports = {};
exports.proposeGeometry = function (term) {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement),
parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height')),
parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17),
@@ -53,7 +56,7 @@
subjectRow.innerHTML = 'W'; // Common character for measuring width, although on monospace
characterWidth = subjectRow.getBoundingClientRect().width;
subjectRow.style.display = ''; // Revert style before calculating height, since they differ.
characterHeight = parseInt(subjectRow.offsetHeight);
characterHeight = subjectRow.getBoundingClientRect().height;
subjectRow.innerHTML = contentBuffer;
rows = parseInt(availableHeight / characterHeight);
@@ -66,7 +69,9 @@
exports.fit = function (term) {
var geometry = exports.proposeGeometry(term);
term.resize(geometry.cols, geometry.rows);
if (geometry) {
term.resize(geometry.cols, geometry.rows);
}
};
Xterm.prototype.proposeGeometry = function () {
-36
View File
@@ -1,36 +0,0 @@
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="../../src/xterm.css" />
<link rel="stylesheet" href="../../demo/style.css" />
<script src="../../src/xterm.js"></script>
<script src="linkify.js"></script>
<style>
body {
color: #111;
}
#terminal-container {
max-width: 900px;
margin: 0 auto;
}
#terminal-container a {
color: #fff;
}
</style>
</head>
<body>
<div id="terminal-container"></div>
<script>
var term = new Terminal(),
container = document.getElementById('terminal-container');
term.open(container);
term.writeln('Hello, welcome to xterm.js');
term.writeln('');
term.writeln('Check it out at github.com/sourcelair/xterm.js');
term.linkify();
</script>
</body>
</html>
-207
View File
@@ -1,207 +0,0 @@
/**
* Methods for turning URL subscrings in the terminal's content into links (`a` DOM elements).
* @module xterm/addons/linkify/linkify
* @license MIT
*/
(function (linkify) {
if (typeof exports === 'object' && typeof module === 'object') {
/*
* CommonJS environment
*/
module.exports = linkify(require('../../xterm'));
} else if (typeof define == 'function') {
/*
* Require.js is available
*/
define(['../../xterm'], linkify);
} else {
/*
* Plain browser environment
*/
linkify(window.Terminal);
}
})(function (Xterm) {
'use strict';
var exports = {},
protocolClause = '(https?:\\/\\/)',
domainCharacterSet = '[\\da-z\\.-]+',
negatedDomainCharacterSet = '[^\\da-z\\.-]+',
domainBodyClause = '(' + domainCharacterSet + ')',
tldClause = '([a-z\\.]{2,6})',
ipClause = '((\\d{1,3}\\.){3}\\d{1,3})',
portClause = '(:\\d{1,5})',
hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + ')' + portClause + '?',
pathClause = '(\\/[\\/\\w\\.-]*)*',
negatedPathCharacterSet = '[^\\/\\w\\.-]+',
bodyClause = hostClause + pathClause,
start = '(?:^|' + negatedDomainCharacterSet + ')(',
end = ')($|' + negatedPathCharacterSet + ')',
lenientUrlClause = start + protocolClause + '?' + bodyClause + end,
strictUrlClause = start + protocolClause + bodyClause + end,
lenientUrlRegex = new RegExp(lenientUrlClause),
strictUrlRegex = new RegExp(strictUrlClause);
/**
* Converts all valid URLs found in the given terminal line into
* hyperlinks. The terminal line can be either the HTML element itself
* or the index of the termina line in the children of the terminal
* rows container.
*
* @param {Xterm} terminal - The terminal that owns the given line.
* @param {number|HTMLDivElement} line - The terminal line that should get
* "linkified".
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
* false, the regex requires a protocol clause. Defaults to true.
* @param {string} target - Sets target="" attribute with value provided to links.
* Default doesn't set target attribute
* @emits linkify
* @emits linkify:line
*/
exports.linkifyTerminalLine = function (terminal, line, lenient, target) {
if (typeof line == 'number') {
line = terminal.rowContainer.children[line];
} else if (! (line instanceof HTMLDivElement)) {
var message = 'The "line" argument should be either a number';
message += ' or an HTMLDivElement';
throw new TypeError(message);
}
if (typeof target === 'undefined') {
target = '';
} else {
target = 'target="' + target + '"';
}
var buffer = document.createElement('span'),
nodes = line.childNodes;
for (var j=0; j<nodes.length; j++) {
var node = nodes[j],
match;
/**
* Since we cannot access the TextNode's HTML representation
* from the instance itself, we assign its data as textContent
* to a dummy buffer span, in order to retrieve the TextNode's
* HTML representation from the buffer's innerHTML.
*/
buffer.textContent = node.data;
var nodeHTML = buffer.innerHTML;
/**
* Apply function only on TextNodes
*/
if (node.nodeType != node.TEXT_NODE) {
continue;
}
var url = exports.findLinkMatch(node.data, lenient);
if (!url) {
continue;
}
var startsWithProtocol = new RegExp('^' + protocolClause),
urlHasProtocol = url.match(startsWithProtocol),
href = (urlHasProtocol) ? url : 'http://' + url,
link = '<a href="' + href + '" ' + target + '>' + url + '</a>',
newHTML = nodeHTML.replace(url, link);
line.innerHTML = line.innerHTML.replace(nodeHTML, newHTML);
}
/**
* This event gets emitted when conversion of all URL susbtrings
* to HTML anchor elements (links) has finished, for a specific
* line of the current Xterm instance.
*
* @event linkify:line
*/
terminal.emit('linkify:line', line);
};
/**
* Finds a link within a block of text.
*
* @param {string} text - The text to search .
* @param {boolean} lenient - Whether to use the lenient search.
* @return {string} A URL.
*/
exports.findLinkMatch = function (text, lenient) {
var match = text.match(lenient ? lenientUrlRegex : strictUrlRegex);
if (!match || match.length === 0) {
return null;
}
return match[1];
}
/**
* Converts all valid URLs found in the terminal view into hyperlinks.
*
* @param {Xterm} terminal - The terminal that should get "linkified".
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
* false, the regex requires a protocol clause. Defaults to true.
* @param {string} target - Sets target="" attribute with value provided to links.
* Default doesn't set target attribute
* @emits linkify
* @emits linkify:line
*/
exports.linkify = function (terminal, lenient, target) {
var rows = terminal.rowContainer.children;
lenient = (typeof lenient == "boolean") ? lenient : true;
for (var i=0; i<rows.length; i++) {
var line = rows[i];
exports.linkifyTerminalLine(terminal, line, lenient, target);
}
/**
* This event gets emitted when conversion of all URL substrings to
* HTML anchor elements (links) has finished for the current Xterm
* instance's view.
*
* @event linkify
*/
terminal.emit('linkify');
};
/**
* Extend Xterm prototype.
*/
/**
* Converts all valid URLs found in the current terminal linte into
* hyperlinks.
*
* @memberof Xterm
* @param {number|HTMLDivElement} line - The terminal line that should get
* "linkified".
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
* false, the regex requires a protocol clause. Defaults to true.
* @param {string} target - Sets target="" attribute with value provided to links.
* Default doesn't set target attribute
*/
Xterm.prototype.linkifyTerminalLine = function (line, lenient, target) {
return exports.linkifyTerminalLine(this, line, lenient, target);
};
/**
* Converts all valid URLs found in the current terminal into hyperlinks.
*
* @memberof Xterm
* @param {boolean} lenient - The regex type that will be used to identify links. If lenient is
* false, the regex requires a protocol clause. Defaults to true.
* @param {string} target - Sets target="" attribute with value provided to links.
* Default doesn't set target attribute
*/
Xterm.prototype.linkify = function (lenient, target) {
return exports.linkify(this, lenient, target);
};
return exports;
});
-5
View File
@@ -1,5 +0,0 @@
{
"name": "xterm.linkify",
"main": "linkify.js",
"private": true
}
+140
View File
@@ -0,0 +1,140 @@
/**
* @license MIT
*/
// import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
interface ISearchResult {
term: string;
col: number;
row: number;
}
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper {
constructor(private _terminal: any, private _translateBufferLineToString: any) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
// TODO: Support regex, case sensitivity, etc.
}
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
public findNext(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionEnd[1];
}
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
public findPrevious(term: string): boolean {
if (!term || term.length === 0) {
return false;
}
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionStart[1];
}
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
}
}
}
// Set selection and scroll if a result was found
return this._selectResult(result);
}
/**
* Searches a line for a search term.
* @param term Tne search term.
* @param y The line to search.
* @return The search result if it was found.
*/
private _findInLine(term: string, y: number): ISearchResult {
const bufferLine = this._terminal.buffer.lines.get(y);
const lowerStringLine = this._translateBufferLineToString(bufferLine, true).toLowerCase();
const lowerTerm = term.toLowerCase();
const searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
return {
term,
col: searchIndex,
row: y
};
}
}
/**
* Selects and scrolls to a result.
* @param result The result to select.
* @return Whethera result was selected.
*/
private _selectResult(result: ISearchResult): boolean {
if (!result) {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollDisp(result.row - this._terminal.buffer.ydisp, false);
return true;
}
}
+57
View File
@@ -0,0 +1,57 @@
/**
* @license MIT
*/
import { SearchHelper } from './SearchHelper';
declare var exports: any;
declare var module: any;
declare var define: any;
declare var require: any;
declare var window: any;
(function (addon) {
if ('Terminal' in window) {
/**
* Plain browser environment
*/
addon(window.Terminal);
} else if (typeof exports === 'object' && typeof module === 'object') {
/**
* CommonJS environment
*/
const xterm = '../../xterm';
module.exports = addon(require(xterm));
} else if (typeof define == 'function') {
/**
* Require.js is available
*/
define(['../../xterm'], addon);
}
})((Terminal: any) => {
/**
* Find the next instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findNext = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
}
return (<SearchHelper>this.searchHelper).findNext(term);
};
/**
* Find the previous instance of the term, then scroll to and select it. If it
* doesn't exist, do nothing.
* @param term Tne search term.
* @return Whether a result was found.
*/
Terminal.prototype.findPrevious = function(term: string): boolean {
if (!this._searchHelper) {
this.searchHelper = new SearchHelper(this, Terminal.translateBufferLineToString);
}
return (<SearchHelper>this.searchHelper).findPrevious(term);
};
});
+10
View File
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true
}
}
+7 -11
View File
@@ -2,17 +2,13 @@ import { assert } from 'chai';
import * as Terminal from '../xterm';
import * as Clipboard from './Clipboard';
describe('evaluatePastedTextProcessing', function () {
it('should replace carriage return + line feed with line feed on windows', function () {
const pastedText = 'foo\r\nbar\r\n',
processedText = Clipboard.prepareTextForTerminal(pastedText, false),
windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
describe('evaluateCopiedTextProcessing', function () {
it('should strip trailing whitespaces and replace nbsps with spaces', function () {
let nonBreakingSpace = String.fromCharCode(160),
copiedText = 'echo' + nonBreakingSpace + 'hello' + nonBreakingSpace,
processedText = Clipboard.prepareTextForClipboard(copiedText);
// No trailing spaces
assert.equal(processedText.match(/\s+$/), null);
// No non-breaking space
assert.equal(processedText.indexOf(nonBreakingSpace), -1);
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\rbar\r');
});
});
+51 -89
View File
@@ -5,7 +5,7 @@
* @license MIT
*/
import { ITerminal } from '../Interfaces';
import { ITerminal, ISelectionManager } from '../Interfaces';
interface IWindow extends Window {
clipboardData?: {
@@ -17,44 +17,29 @@ interface IWindow extends Window {
declare var window: IWindow;
/**
* Prepares text copied from terminal selection, to be saved in the clipboard by:
* 1. stripping all trailing white spaces
* 2. converting all non-breaking spaces to regular spaces
* @param {string} text The copied text that needs processing for storing in clipboard
* @returns {string}
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForClipboard(text: string): string {
let space = String.fromCharCode(32),
nonBreakingSpace = String.fromCharCode(160),
allNonBreakingSpaces = new RegExp(nonBreakingSpace, 'g'),
processedText = text.split('\n').map(function (line) {
// Strip all trailing white spaces and convert all non-breaking spaces
// to regular spaces.
let processedLine = line.replace(/\s+$/g, '').replace(allNonBreakingSpaces, space);
return processedLine;
}).join('\n');
return processedText;
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
if (isMSWindows) {
return text.replace(/\r?\n/g, '\r');
}
return text;
}
/**
* Binds copy functionality to the given terminal.
* @param {ClipboardEvent} ev The original copy event to be handled
*/
export function copyHandler(ev: ClipboardEvent, term: ITerminal) {
// We cast `window` to `any` type, because TypeScript has not declared the `clipboardData`
// property that we use below for Internet Explorer.
let copiedText = window.getSelection().toString(),
text = prepareTextForClipboard(copiedText);
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager) {
if (term.browser.isMSIE) {
window.clipboardData.setData('Text', text);
window.clipboardData.setData('Text', selectionManager.selectionText);
} else {
ev.clipboardData.setData('text/plain', text);
ev.clipboardData.setData('text/plain', selectionManager.selectionText);
}
ev.preventDefault(); // Prevent or the original text will be copied.
// Prevent or the original text will be copied.
ev.preventDefault();
}
/**
@@ -68,8 +53,11 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
let text: string;
let dispatchPaste = function(text) {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
term.handler(text);
term.textarea.value = '';
term.emit('paste', text);
return term.cancel(ev);
};
@@ -87,68 +75,42 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal) {
}
/**
* Bind to right-click event and allow right-click copy and paste.
*
* **Logic**
* If text is selected and right-click happens on selected text, then
* do nothing to allow seamless copying.
* If no text is selected or right-click is outside of the selection
* area, then bring the terminal's input below the cursor, in order to
* trigger the event on the textarea and allow-right click paste, without
* caring about disappearing selection.
* @param {MouseEvent} ev The original right click event to be handled
* @param {Terminal} term The terminal on which to apply the handled paste event
* Moves the textarea under the mouse cursor and focuses it.
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
*/
export function rightClickHandler(ev: MouseEvent, term: ITerminal) {
let s = document.getSelection(),
selectedText = prepareTextForClipboard(s.toString()),
clickIsOnSelection = false,
x = ev.clientX,
y = ev.clientY;
if (s.rangeCount) {
let r = s.getRangeAt(0),
cr = r.getClientRects();
for (let i = 0; i < cr.length; i++) {
let rect = cr[i];
clickIsOnSelection = (
(x > rect.left) && (x < rect.right) &&
(y > rect.top) && (y < rect.bottom)
);
if (clickIsOnSelection) {
break;
}
}
// If we clicked on selection and selection is not a single space,
// then mark the right click as copy-only. We check for the single
// space selection, as this can happen when clicking on an &nbsp;
// and there is not much pointing in copying a single space.
if (selectedText.match(/^\s$/) || !selectedText.length) {
clickIsOnSelection = false;
}
}
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement) {
// Bring textarea at the cursor position
if (!clickIsOnSelection) {
term.textarea.style.position = 'fixed';
term.textarea.style.width = '20px';
term.textarea.style.height = '20px';
term.textarea.style.left = (x - 10) + 'px';
term.textarea.style.top = (y - 10) + 'px';
term.textarea.style.zIndex = '1000';
term.textarea.focus();
textarea.style.position = 'fixed';
textarea.style.width = '20px';
textarea.style.height = '20px';
textarea.style.left = (ev.clientX - 10) + 'px';
textarea.style.top = (ev.clientY - 10) + 'px';
textarea.style.zIndex = '1000';
// Reset the terminal textarea's styling
setTimeout(function () {
term.textarea.style.position = null;
term.textarea.style.width = null;
term.textarea.style.height = null;
term.textarea.style.left = null;
term.textarea.style.top = null;
term.textarea.style.zIndex = null;
}, 4);
}
textarea.focus();
// Reset the terminal textarea's styling
setTimeout(function () {
textarea.style.position = null;
textarea.style.width = null;
textarea.style.height = null;
textarea.style.left = null;
textarea.style.top = null;
textarea.style.zIndex = null;
}, 4);
}
/**
* Bind to right-click event and allow right-click copy and paste.
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionManager The terminal's selection manager.
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager) {
moveTextAreaUnderMouseCursor(ev, textarea);
// Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText;
textarea.select();
}
-48
View File
@@ -1,48 +0,0 @@
var assert = require('chai').assert;
var Terminal = require('../../xterm');
var linkify = require('../../addons/linkify/linkify');
describe('linkify addon', function () {
var xterm;
describe('API', function () {
it('should define Terminal.prototype.linkify', function () {
assert.isDefined(Terminal.prototype.linkify);
});
it('should define Terminal.prototype.linkifyTerminalLine', function () {
assert.isDefined(Terminal.prototype.linkifyTerminalLine);
});
});
describe('findUrlMatchOnLine', function () {
describe('strict regex', function () {
it('should match when the entire text is a match', function () {
assert.equal(linkify.findLinkMatch('http://github.com', false), 'http://github.com');
assert.equal(linkify.findLinkMatch('http://127.0.0.1', false), 'http://127.0.0.1');
});
it('should match simple domains', function () {
assert.equal(linkify.findLinkMatch('foo http://github.com bar', false), 'http://github.com');
assert.equal(linkify.findLinkMatch('foo http://www.github.com bar', false), 'http://www.github.com');
assert.equal(linkify.findLinkMatch('foo https://github.com bar', false), 'https://github.com');
assert.equal(linkify.findLinkMatch('foo https://www.github.com bar', false), 'https://www.github.com');
});
it('should match web addresses with alpha paths', function () {
assert.equal(linkify.findLinkMatch('foo http://github.com/a/b/c bar', false), 'http://github.com/a/b/c');
assert.equal(linkify.findLinkMatch('foo http://www.github.com/a/b/c bar', false), 'http://www.github.com/a/b/c');
});
it('should not include whitespace surrounding a match', function () {
assert.equal(linkify.findLinkMatch(' http://github.com', false), 'http://github.com');
assert.equal(linkify.findLinkMatch('http://github.com ', false), 'http://github.com');
assert.equal(linkify.findLinkMatch(' http://github.com ', false), 'http://github.com');
});
it('should match IP addresses', function () {
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1 bar', false), 'http://127.0.0.1');
assert.equal(linkify.findLinkMatch('foo https://127.0.0.1 bar', false), 'https://127.0.0.1');
});
it('should match ports on both domains and IP addresses', function () {
assert.equal(linkify.findLinkMatch('foo http://127.0.0.1:8080 bar', false), 'http://127.0.0.1:8080');
assert.equal(linkify.findLinkMatch('foo http://www.github.com:8080 bar', false), 'http://www.github.com:8080');
});
});
});
});
+39 -32
View File
@@ -2,7 +2,6 @@ var glob = require('glob');
var fs = require('fs');
var os = require('os');
var pty = require('node-pty');
var sleep = require('sleep');
var Terminal = require('../xterm');
if (os.platform() === 'win32') {
@@ -25,17 +24,18 @@ var primitive_pty = pty.native.open(COLS, ROWS);
// we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode
function pty_write_read(s) {
function ptyWriteRead(s, cb) {
fs.writeSync(primitive_pty.slave, s);
sleep.usleep(10000);
var b = Buffer(64000);
var bytes = fs.readSync(primitive_pty.master, b, 0, 64000);
return b.toString('utf8', 0, bytes);
setTimeout(() => {
var b = Buffer(64000);
var bytes = fs.readSync(primitive_pty.master, b, 0, 64000);
cb(b.toString('utf8', 0, bytes));
});
}
// make sure raw pty is at x=0 and has no pending data
function pty_reset() {
pty_write_read('\r\n');
function ptyReset(cb) {
ptyWriteRead('\r\n', cb);
}
/* debug helpers */
@@ -62,10 +62,10 @@ function formatError(in_, out_, expected) {
function terminalToString(term) {
var result = '';
var line_s = '';
for (var line = term.ybase; line < term.ybase + term.rows; line++) {
for (var line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
line_s = '';
for (var cell=0; cell<term.cols; ++cell) {
line_s += term.lines.get(line)[cell][1];
line_s += term.buffer.lines.get(line)[cell][1];
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');
@@ -97,35 +97,42 @@ describe('xterm output comparison', function() {
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
63, 68
];
if (os.platform() === 'darwin') {
// These are failing on macOS only
skip.push(3, 7, 11, 67);
}
for (var i = 0; i < files.length; i++) {
if (skip.indexOf(i) >= 0) {
continue;
}
(function(filename) {
it(filename.split('/').slice(-1)[0], function () {
pty_reset();
var in_file = fs.readFileSync(filename, 'utf8');
var from_pty = pty_write_read(in_file);
// uncomment this to get log from terminal
//console.log = function(){};
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
var in_file = fs.readFileSync(filename, 'utf8');
ptyWriteRead(in_file, from_pty => {
// uncomment this to get log from terminal
//console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(from_pty);
xterm.innerWrite();
// Perform a synchronous .write(data)
xterm.writeBuffer.push(from_pty);
xterm.innerWrite();
var from_emulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
var expectedRightTrimmed = expected.split('\n').map(function (l) {
return l.replace(/\s+$/, '');
}).join('\n');
if (from_emulator != expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(in_file, from_emulator, expected));
// throw new Error('mismatch');
}
var from_emulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
var expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
var expectedRightTrimmed = expected.split('\n').map(function (l) {
return l.replace(/\s+$/, '');
}).join('\n');
if (from_emulator != expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(in_file, from_emulator, expected));
// throw new Error('mismatch');
}
done();
});
});
});
})(files[i]);
}
+156 -134
View File
@@ -21,7 +21,8 @@ describe('xterm.js', function() {
};
xterm.element = {
classList: {
toggle: function(){}
toggle: function(){},
remove: function(){}
}
};
});
@@ -54,19 +55,24 @@ describe('xterm.js', function() {
it('should throw when setting a non-existant option', function() {
assert.throws(xterm.setOption.bind(xterm, 'fake', true));
});
it('should not allow scrollback less than number of rows', function() {
let setOptionCall = xterm.setOption.bind(xterm, 'scrollback', xterm.rows - 1);
assert.equal(setOptionCall(), false);
});
});
describe('clear', function() {
it('should clear a buffer equal to rows', function() {
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
xterm.clear();
assert.equal(xterm.y, 0);
assert.equal(xterm.ybase, 0);
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.lines.length, xterm.rows);
assert.deepEqual(xterm.lines.get(0), promptLine);
assert.equal(xterm.buffer.y, 0);
assert.equal(xterm.buffer.ybase, 0);
assert.equal(xterm.buffer.ydisp, 0);
assert.equal(xterm.buffer.lines.length, xterm.rows);
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
for (var i = 1; i < xterm.rows; i++) {
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
}
});
it('should clear a buffer larger than rows', function() {
@@ -75,28 +81,28 @@ describe('xterm.js', function() {
xterm.write('test\n');
}
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
xterm.clear();
assert.equal(xterm.y, 0);
assert.equal(xterm.ybase, 0);
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.lines.length, xterm.rows);
assert.deepEqual(xterm.lines.get(0), promptLine);
assert.equal(xterm.buffer.y, 0);
assert.equal(xterm.buffer.ybase, 0);
assert.equal(xterm.buffer.ydisp, 0);
assert.equal(xterm.buffer.lines.length, xterm.rows);
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
for (var i = 1; i < xterm.rows; i++) {
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
}
});
it('should not break the prompt when cleared twice', function() {
var promptLine = xterm.lines.get(xterm.ybase + xterm.y);
var promptLine = xterm.buffer.lines.get(xterm.buffer.ybase + xterm.buffer.y);
xterm.clear();
xterm.clear();
assert.equal(xterm.y, 0);
assert.equal(xterm.ybase, 0);
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.lines.length, xterm.rows);
assert.deepEqual(xterm.lines.get(0), promptLine);
assert.equal(xterm.buffer.y, 0);
assert.equal(xterm.buffer.ybase, 0);
assert.equal(xterm.buffer.ydisp, 0);
assert.equal(xterm.buffer.lines.length, xterm.rows);
assert.deepEqual(xterm.buffer.lines.get(0), promptLine);
for (var i = 1; i < xterm.rows; i++) {
assert.deepEqual(xterm.lines.get(i), xterm.blankLine());
assert.deepEqual(xterm.buffer.lines.get(i), xterm.blankLine());
}
});
});
@@ -111,29 +117,29 @@ describe('xterm.js', function() {
startYDisp = xterm.rows + 1;
});
it('should scroll a single line', function() {
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollDisp(-1);
assert.equal(xterm.ydisp, startYDisp - 1);
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
xterm.scrollDisp(1);
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
});
it('should scroll multiple lines', function() {
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollDisp(-5);
assert.equal(xterm.ydisp, startYDisp - 5);
assert.equal(xterm.buffer.ydisp, startYDisp - 5);
xterm.scrollDisp(5);
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
});
it('should not scroll beyond the bounds of the buffer', function() {
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollDisp(1);
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
for (var i = 0; i < startYDisp; i++) {
xterm.scrollDisp(-1);
}
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.buffer.ydisp, 0);
xterm.scrollDisp(-1);
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.buffer.ydisp, 0);
});
});
@@ -146,18 +152,18 @@ describe('xterm.js', function() {
startYDisp = (xterm.rows * 2) + 1;
});
it('should scroll a single page', function() {
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollPages(-1);
assert.equal(xterm.ydisp, startYDisp - (xterm.rows - 1));
assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1));
xterm.scrollPages(1);
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
});
it('should scroll a multiple pages', function() {
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollPages(-2);
assert.equal(xterm.ydisp, startYDisp - (xterm.rows - 1) * 2);
assert.equal(xterm.buffer.ydisp, startYDisp - (xterm.rows - 1) * 2);
xterm.scrollPages(2);
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
});
});
@@ -168,9 +174,9 @@ describe('xterm.js', function() {
}
});
it('should scroll to the top', function() {
assert.notEqual(xterm.ydisp, 0);
assert.notEqual(xterm.buffer.ydisp, 0);
xterm.scrollToTop();
assert.equal(xterm.ydisp, 0);
assert.equal(xterm.buffer.ydisp, 0);
});
});
@@ -185,13 +191,13 @@ describe('xterm.js', function() {
it('should scroll to the bottom', function() {
xterm.scrollDisp(-1);
xterm.scrollToBottom();
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollPages(-1);
xterm.scrollToBottom();
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollToTop();
xterm.scrollToBottom();
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
});
});
@@ -208,13 +214,13 @@ describe('xterm.js', function() {
stopPropagation: function(){}
};
xterm.ydisp = 0;
xterm.ybase = 40;
assert.notEqual(xterm.ydisp, xterm.ybase);
xterm.buffer.ydisp = 0;
xterm.buffer.ybase = 40;
assert.notEqual(xterm.buffer.ydisp, xterm.buffer.ybase);
xterm.keyDown(event);
// Ensure that now the terminal is scrolled to bottom
assert.equal(xterm.ydisp, xterm.ybase);
assert.equal(xterm.buffer.ydisp, xterm.buffer.ybase);
});
it('should not scroll down, when a custom keydown handler prevents the event', function () {
@@ -227,11 +233,11 @@ describe('xterm.js', function() {
return false;
});
assert.equal(xterm.ydisp, startYDisp);
assert.equal(xterm.buffer.ydisp, startYDisp);
xterm.scrollDisp(-1);
assert.equal(xterm.ydisp, startYDisp - 1);
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
xterm.keyDown({ keyCode: 0 });
assert.equal(xterm.ydisp, startYDisp - 1);
assert.equal(xterm.buffer.ydisp, startYDisp - 1);
});
});
});
@@ -370,12 +376,17 @@ describe('xterm.js', function() {
});
});
describe('attachCustomEventHandler', function () {
describe('attachCustomKeyEventHandler', function () {
var evKeyDown = {
preventDefault: function() {},
stopPropagation: function() {},
type: 'keydown'
}
var evKeyPress = {
preventDefault: function() {},
stopPropagation: function() {},
type: 'keypress'
}
beforeEach(function() {
xterm.handler = function() {};
@@ -386,29 +397,39 @@ describe('xterm.js', function() {
bind: function() {
return function () { return true; }
}
},
keypress: {
bind: function() {
return function () { return true; }
}
}
}
});
it('should process the keydown event based on what the handler returns', function () {
it('should process the keydown/keypress event based on what the handler returns', function () {
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
xterm.attachCustomKeydownHandler(function (ev) {
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
xterm.attachCustomKeyEventHandler(function (ev) {
return ev.keyCode === 77;
});
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), true);
xterm.attachCustomKeydownHandler(function (ev) {
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), true);
xterm.attachCustomKeyEventHandler(function (ev) {
return ev.keyCode !== 77;
});
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
});
it('should alive after reset(ESC c Full Reset (RIS))', function () {
xterm.attachCustomKeydownHandler(function (ev) {
xterm.attachCustomKeyEventHandler(function (ev) {
return ev.keyCode !== 77;
});
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
xterm.reset();
assert.equal(xterm.keyDown(Object.assign({}, evKeyDown, { keyCode: 77 })), false);
assert.equal(xterm.keyPress(Object.assign({}, evKeyPress, { keyCode: 77 })), false);
});
});
@@ -550,51 +571,52 @@ describe('xterm.js', function() {
describe('unicode - surrogates', function() {
it('2 characters per cell', function () {
this.timeout(10000); // This is needed because istanbul patches code and slows it down
var high = String.fromCharCode(0xD800);
for (var i=0xDC00; i<=0xDCFF; ++i) {
xterm.write(high + String.fromCharCode(i));
var tchar = xterm.lines.get(0)[0];
var tchar = xterm.buffer.lines.get(0)[0];
expect(tchar[1]).eql(high + String.fromCharCode(i));
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(1);
expect(xterm.lines.get(0)[1][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
xterm.reset();
}
});
it('2 characters at last cell', function() {
var high = String.fromCharCode(0xD800);
for (var i=0xDC00; i<=0xDCFF; ++i) {
xterm.x = xterm.cols - 1;
xterm.buffer.x = xterm.cols - 1;
xterm.write(high + String.fromCharCode(i));
expect(xterm.lines.get(0)[xterm.x-1][1]).eql(high + String.fromCharCode(i));
expect(xterm.lines.get(0)[xterm.x-1][1].length).eql(2);
expect(xterm.lines.get(1)[0][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][1]).eql(high + String.fromCharCode(i));
expect(xterm.buffer.lines.get(0)[xterm.buffer.x-1][1].length).eql(2);
expect(xterm.buffer.lines.get(1)[0][1]).eql(' ');
xterm.reset();
}
});
it('2 characters per cell over line end with autowrap', function() {
var high = String.fromCharCode(0xD800);
for (var i=0xDC00; i<=0xDCFF; ++i) {
xterm.x = xterm.cols - 1;
xterm.buffer.x = xterm.cols - 1;
xterm.wraparoundMode = true;
xterm.write('a' + high + String.fromCharCode(i));
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
expect(xterm.lines.get(1)[0][1]).eql(high + String.fromCharCode(i));
expect(xterm.lines.get(1)[0][1].length).eql(2);
expect(xterm.lines.get(1)[1][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('a');
expect(xterm.buffer.lines.get(1)[0][1]).eql(high + String.fromCharCode(i));
expect(xterm.buffer.lines.get(1)[0][1].length).eql(2);
expect(xterm.buffer.lines.get(1)[1][1]).eql(' ');
xterm.reset();
}
});
it('2 characters per cell over line end without autowrap', function() {
var high = String.fromCharCode(0xD800);
for (var i=0xDC00; i<=0xDCFF; ++i) {
xterm.x = xterm.cols - 1;
xterm.buffer.x = xterm.cols - 1;
xterm.wraparoundMode = false;
xterm.write('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('a');
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(1);
expect(xterm.lines.get(1)[1][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('a');
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1].length).eql(1);
expect(xterm.buffer.lines.get(1)[1][1]).eql(' ');
xterm.reset();
}
});
@@ -603,11 +625,11 @@ describe('xterm.js', function() {
for (var i=0xDC00; i<=0xDCFF; ++i) {
xterm.write(high);
xterm.write(String.fromCharCode(i));
var tchar = xterm.lines.get(0)[0];
var tchar = xterm.buffer.lines.get(0)[0];
expect(tchar[1]).eql(high + String.fromCharCode(i));
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(1);
expect(xterm.lines.get(0)[1][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
xterm.reset();
}
});
@@ -616,30 +638,30 @@ describe('xterm.js', function() {
describe('unicode - combining characters', function() {
it('café', function () {
xterm.write('cafe\u0301');
expect(xterm.lines.get(0)[3][1]).eql('e\u0301');
expect(xterm.lines.get(0)[3][1].length).eql(2);
expect(xterm.lines.get(0)[3][2]).eql(1);
expect(xterm.buffer.lines.get(0)[3][1]).eql('e\u0301');
expect(xterm.buffer.lines.get(0)[3][1].length).eql(2);
expect(xterm.buffer.lines.get(0)[3][2]).eql(1);
});
it('café - end of line', function() {
xterm.x = xterm.cols - 1 - 3;
xterm.buffer.x = xterm.cols - 1 - 3;
xterm.write('cafe\u0301');
expect(xterm.lines.get(0)[xterm.cols-1][1]).eql('e\u0301');
expect(xterm.lines.get(0)[xterm.cols-1][1].length).eql(2);
expect(xterm.lines.get(0)[xterm.cols-1][2]).eql(1);
expect(xterm.lines.get(0)[1][1]).eql(' ');
expect(xterm.lines.get(0)[1][1].length).eql(1);
expect(xterm.lines.get(0)[1][2]).eql(1);
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1]).eql('e\u0301');
expect(xterm.buffer.lines.get(0)[xterm.cols-1][1].length).eql(2);
expect(xterm.buffer.lines.get(0)[xterm.cols-1][2]).eql(1);
expect(xterm.buffer.lines.get(0)[1][1]).eql(' ');
expect(xterm.buffer.lines.get(0)[1][1].length).eql(1);
expect(xterm.buffer.lines.get(0)[1][2]).eql(1);
});
it('multiple combined é', function() {
xterm.wraparoundMode = true;
xterm.write(Array(100).join('e\u0301'));
for (var i=0; i<xterm.cols; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
expect(tchar[1]).eql('e\u0301');
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(1);
}
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('e\u0301');
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(1);
@@ -648,12 +670,12 @@ describe('xterm.js', function() {
xterm.wraparoundMode = true;
xterm.write(Array(100).join('\uD800\uDC00\u0301'));
for (var i=0; i<xterm.cols; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
expect(tchar[1]).eql('\uD800\uDC00\u0301');
expect(tchar[1].length).eql(3);
expect(tchar[2]).eql(1);
}
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('\uD800\uDC00\u0301');
expect(tchar[1].length).eql(3);
expect(tchar[2]).eql(1);
@@ -662,21 +684,21 @@ describe('xterm.js', function() {
describe('unicode - fullwidth characters', function() {
it('cursor movement even', function() {
expect(xterm.x).eql(0);
expect(xterm.buffer.x).eql(0);
xterm.write('¥');
expect(xterm.x).eql(2);
expect(xterm.buffer.x).eql(2);
});
it('cursor movement odd', function() {
xterm.x = 1;
expect(xterm.x).eql(1);
xterm.buffer.x = 1;
expect(xterm.buffer.x).eql(1);
xterm.write('¥');
expect(xterm.x).eql(3);
expect(xterm.buffer.x).eql(3);
});
it('line of ¥ even', function() {
xterm.wraparoundMode = true;
xterm.write(Array(50).join('¥'));
for (var i=0; i<xterm.cols; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -687,17 +709,17 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('¥');
expect(tchar[1].length).eql(1);
expect(tchar[2]).eql(2);
});
it('line of ¥ odd', function() {
xterm.wraparoundMode = true;
xterm.x = 1;
xterm.buffer.x = 1;
xterm.write(Array(50).join('¥'));
for (var i=1; i<xterm.cols-1; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -708,21 +730,21 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(0)[xterm.cols-1];
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
expect(tchar[1]).eql(' ');
expect(tchar[1].length).eql(1);
expect(tchar[2]).eql(1);
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('¥');
expect(tchar[1].length).eql(1);
expect(tchar[2]).eql(2);
});
it('line of ¥ with combining odd', function() {
xterm.wraparoundMode = true;
xterm.x = 1;
xterm.buffer.x = 1;
xterm.write(Array(50).join('¥\u0301'));
for (var i=1; i<xterm.cols-1; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -733,11 +755,11 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(0)[xterm.cols-1];
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
expect(tchar[1]).eql(' ');
expect(tchar[1].length).eql(1);
expect(tchar[2]).eql(1);
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('¥\u0301');
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(2);
@@ -746,7 +768,7 @@ describe('xterm.js', function() {
xterm.wraparoundMode = true;
xterm.write(Array(50).join('¥\u0301'));
for (var i=0; i<xterm.cols; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -757,17 +779,17 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('¥\u0301');
expect(tchar[1].length).eql(2);
expect(tchar[2]).eql(2);
});
it('line of surrogate fullwidth with combining odd', function() {
xterm.wraparoundMode = true;
xterm.x = 1;
xterm.buffer.x = 1;
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
for (var i=1; i<xterm.cols-1; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -778,11 +800,11 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(0)[xterm.cols-1];
tchar = xterm.buffer.lines.get(0)[xterm.cols-1];
expect(tchar[1]).eql(' ');
expect(tchar[1].length).eql(1);
expect(tchar[2]).eql(1);
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('\ud843\ude6d\u0301');
expect(tchar[1].length).eql(3);
expect(tchar[2]).eql(2);
@@ -791,7 +813,7 @@ describe('xterm.js', function() {
xterm.wraparoundMode = true;
xterm.write(Array(50).join('\ud843\ude6d\u0301'));
for (var i=0; i<xterm.cols; ++i) {
var tchar = xterm.lines.get(0)[i];
var tchar = xterm.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[1]).eql('');
expect(tchar[1].length).eql(0);
@@ -802,7 +824,7 @@ describe('xterm.js', function() {
expect(tchar[2]).eql(2);
}
}
tchar = xterm.lines.get(1)[0];
tchar = xterm.buffer.lines.get(1)[0];
expect(tchar[1]).eql('\ud843\ude6d\u0301');
expect(tchar[1].length).eql(3);
expect(tchar[2]).eql(2);
@@ -812,44 +834,44 @@ describe('xterm.js', function() {
describe('insert mode', function() {
it('halfwidth - all', function () {
xterm.write(Array(9).join('0123456789').slice(-80));
xterm.x = 10;
xterm.y = 0;
xterm.buffer.x = 10;
xterm.buffer.y = 0;
xterm.insertMode = true;
xterm.write('abcde');
expect(xterm.lines.get(0).length).eql(xterm.cols);
expect(xterm.lines.get(0)[10][1]).eql('a');
expect(xterm.lines.get(0)[14][1]).eql('e');
expect(xterm.lines.get(0)[15][1]).eql('0');
expect(xterm.lines.get(0)[79][1]).eql('4');
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
expect(xterm.buffer.lines.get(0)[10][1]).eql('a');
expect(xterm.buffer.lines.get(0)[14][1]).eql('e');
expect(xterm.buffer.lines.get(0)[15][1]).eql('0');
expect(xterm.buffer.lines.get(0)[79][1]).eql('4');
});
it('fullwidth - insert', function() {
xterm.write(Array(9).join('0123456789').slice(-80));
xterm.x = 10;
xterm.y = 0;
xterm.buffer.x = 10;
xterm.buffer.y = 0;
xterm.insertMode = true;
xterm.write('¥¥¥');
expect(xterm.lines.get(0).length).eql(xterm.cols);
expect(xterm.lines.get(0)[10][1]).eql('¥');
expect(xterm.lines.get(0)[11][1]).eql('');
expect(xterm.lines.get(0)[14][1]).eql('¥');
expect(xterm.lines.get(0)[15][1]).eql('');
expect(xterm.lines.get(0)[79][1]).eql('3');
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
expect(xterm.buffer.lines.get(0)[10][1]).eql('¥');
expect(xterm.buffer.lines.get(0)[11][1]).eql('');
expect(xterm.buffer.lines.get(0)[14][1]).eql('¥');
expect(xterm.buffer.lines.get(0)[15][1]).eql('');
expect(xterm.buffer.lines.get(0)[79][1]).eql('3');
});
it('fullwidth - right border', function() {
xterm.write(Array(41).join('¥'));
xterm.x = 10;
xterm.y = 0;
xterm.buffer.x = 10;
xterm.buffer.y = 0;
xterm.insertMode = true;
xterm.write('a');
expect(xterm.lines.get(0).length).eql(xterm.cols);
expect(xterm.lines.get(0)[10][1]).eql('a');
expect(xterm.lines.get(0)[11][1]).eql('¥');
expect(xterm.lines.get(0)[79][1]).eql(' '); // fullwidth char got replaced
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
expect(xterm.buffer.lines.get(0)[10][1]).eql('a');
expect(xterm.buffer.lines.get(0)[11][1]).eql('¥');
expect(xterm.buffer.lines.get(0)[79][1]).eql(' '); // fullwidth char got replaced
xterm.write('b');
expect(xterm.lines.get(0).length).eql(xterm.cols);
expect(xterm.lines.get(0)[11][1]).eql('b');
expect(xterm.lines.get(0)[12][1]).eql('¥');
expect(xterm.lines.get(0)[79][1]).eql(''); // empty cell after fullwidth
expect(xterm.buffer.lines.get(0).length).eql(xterm.cols);
expect(xterm.buffer.lines.get(0)[11][1]).eql('b');
expect(xterm.buffer.lines.get(0)[12][1]).eql('¥');
expect(xterm.buffer.lines.get(0)[79][1]).eql(''); // empty cell after fullwidth
});
});
});
+1
View File
@@ -20,3 +20,4 @@ export const isMac = contains(['Macintosh', 'MacIntel', 'MacPPC', 'Mac68K'], pla
export const isIpad = platform === 'iPad';
export const isIphone = platform === 'iPhone';
export const isMSWindows = contains(['Windows', 'Win16', 'Win32', 'WinCE'], platform);
export const isLinux = platform.indexOf('Linux') >= 0;
+55
View File
@@ -0,0 +1,55 @@
/**
* @license MIT
*/
// TODO: This module should be merged into a buffer or buffer line class
const LINE_DATA_CHAR_INDEX = 1;
const LINE_DATA_WIDTH_INDEX = 2;
/**
* Translates a buffer line to a string, with optional start and end columns.
* Wide characters will count as two columns in the resulting string. This
* function is useful for getting the actual text underneath the raw selection
* position.
* @param line The line being translated.
* @param trimRight Whether to trim whitespace to the right.
* @param startCol The column to start at.
* @param endCol The column to end at.
*/
export function translateBufferLineToString(line: any, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
// Get full line
let lineString = '';
let widthAdjustedStartCol = startCol;
let widthAdjustedEndCol = endCol;
for (let i = 0; i < line.length; i++) {
const char = line[i];
lineString += char[LINE_DATA_CHAR_INDEX];
// Adjust start and end cols for wide characters if they affect their
// column indexes
if (char[LINE_DATA_WIDTH_INDEX] === 0) {
if (startCol >= i) {
widthAdjustedStartCol--;
}
if (endCol >= i) {
widthAdjustedEndCol--;
}
}
}
// Calculate the final end col by trimming whitespace on the right of the
// line if needed.
let finalEndCol = widthAdjustedEndCol || line.length;
if (trimRight) {
const rightWhitespaceIndex = lineString.search(/\s+$/);
if (rightWhitespaceIndex !== -1) {
finalEndCol = Math.min(finalEndCol, rightWhitespaceIndex);
}
// Return the empty string if only trimmed whitespace is selected
if (finalEndCol <= widthAdjustedStartCol) {
return '';
}
}
return lineString.substring(widthAdjustedStartCol, finalEndCol);
}
+75
View File
@@ -0,0 +1,75 @@
/**
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ICharMeasure, ITerminal } from '../Interfaces';
import { CharMeasure } from './CharMeasure';
describe('CharMeasure', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let container: HTMLElement;
let charMeasure: ICharMeasure;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
container = document.createElement('div');
document.body.appendChild(container);
charMeasure = new CharMeasure(document, container);
});
describe('measure', () => {
it('should set _measureElement on first call', () => {
charMeasure.measure();
assert.isDefined((<any>charMeasure)._measureElement, 'CharMeasure.measure should have created _measureElement');
});
it('should be performed async on first call', done => {
assert.equal(charMeasure.width, null);
charMeasure.measure();
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
};
assert.equal(charMeasure.width, null);
setTimeout(() => {
assert.equal(charMeasure.width, 1);
done();
}, 0);
});
it('should be performed sync on successive calls', done => {
charMeasure.measure();
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 1, height: 1 };
};
setTimeout(() => {
const firstWidth = charMeasure.width;
// Mock getBoundingClientRect since jsdom doesn't have a layout engine
(<any>charMeasure)._measureElement.getBoundingClientRect = () => {
return { width: 2, height: 2 };
};
charMeasure.measure();
assert.equal(charMeasure.width, firstWidth * 2);
done();
}, 0);
});
it('should NOT do a measure when the parent is hidden', done => {
charMeasure.measure();
setTimeout(() => {
const firstWidth = charMeasure.width;
container.style.display = 'none';
container.style.fontSize = '2em';
charMeasure.measure();
assert.equal(charMeasure.width, firstWidth);
done();
}, 0);
});
});
});
+5 -2
View File
@@ -9,13 +9,15 @@ import { EventEmitter } from '../EventEmitter.js';
* Utility class that measures the size of a character.
*/
export class CharMeasure extends EventEmitter {
private _document: Document;
private _parentElement: HTMLElement;
private _measureElement: HTMLElement;
private _width: number;
private _height: number;
constructor(parentElement: HTMLElement) {
constructor(document: Document, parentElement: HTMLElement) {
super();
this._document = document;
this._parentElement = parentElement;
}
@@ -29,11 +31,12 @@ export class CharMeasure extends EventEmitter {
public measure(): void {
if (!this._measureElement) {
this._measureElement = document.createElement('span');
this._measureElement = this._document.createElement('span');
this._measureElement.style.position = 'absolute';
this._measureElement.style.top = '0';
this._measureElement.style.left = '-9999em';
this._measureElement.textContent = 'W';
this._measureElement.setAttribute('aria-hidden', 'true');
this._parentElement.appendChild(this._measureElement);
// Perform _doMeasure async if the element was just attached as sometimes
// getBoundingClientRect does not return accurate values without this.
+22 -4
View File
@@ -4,12 +4,15 @@
* @module xterm/utils/CircularList
* @license MIT
*/
export class CircularList<T> {
import { EventEmitter } from '../EventEmitter';
export class CircularList<T> extends EventEmitter {
private _array: T[];
private _startIndex: number;
private _length: number;
constructor(maxLength: number) {
super();
this._array = new Array<T>(maxLength);
this._startIndex = 0;
this._length = 0;
@@ -43,8 +46,14 @@ export class CircularList<T> {
this._length = newLength;
}
public get forEach(): (callbackfn: (value: T, index: number, array: T[]) => void) => void {
return this._array.forEach;
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
return (callbackfn: (value: T, index: number) => void) => {
let i = 0;
let length = this.length;
for (let i = 0; i < length; i++) {
callbackfn(this.get(i), i);
}
};
}
/**
@@ -83,6 +92,7 @@ export class CircularList<T> {
if (this._startIndex === this.maxLength) {
this._startIndex = 0;
}
this.emit('trim', 1);
} else {
this._length++;
}
@@ -106,13 +116,16 @@ export class CircularList<T> {
* @param items The items to insert.
*/
public splice(start: number, deleteCount: number, ...items: T[]): void {
// Delete items
if (deleteCount) {
for (let i = start; i < this._length - deleteCount; i++) {
this._array[this._getCyclicIndex(i)] = this._array[this._getCyclicIndex(i + deleteCount)];
}
this._length -= deleteCount;
}
if (items && items.length) {
// Add items
for (let i = this._length - 1; i >= start; i--) {
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
}
@@ -120,9 +133,12 @@ export class CircularList<T> {
this._array[this._getCyclicIndex(start + i)] = items[i];
}
// Adjust length as needed
if (this._length + items.length > this.maxLength) {
this._startIndex += (this._length + items.length) - this.maxLength;
const countToTrim = (this._length + items.length) - this.maxLength;
this._startIndex += countToTrim;
this._length = this.maxLength;
this.emit('trim', countToTrim);
} else {
this._length += items.length;
}
@@ -139,6 +155,7 @@ export class CircularList<T> {
}
this._startIndex += count;
this._length -= count;
this.emit('trim', count);
}
public shiftElements(start: number, count: number, offset: number): void {
@@ -162,6 +179,7 @@ export class CircularList<T> {
while (this._length > this.maxLength) {
this._length--;
this._startIndex++;
this.emit('trim', 1);
}
}
} else {
+47
View File
@@ -0,0 +1,47 @@
import { assert } from 'chai';
import { DomElementObjectPool } from './DomElementObjectPool';
class MockDocument {
private _attr: {[key: string]: string} = {};
constructor() {}
public getAttribute(key: string): string { return this._attr[key]; };
public setAttribute(key: string, value: string): void { this._attr[key] = value; }
}
describe('DomElementObjectPool', () => {
let pool: DomElementObjectPool;
beforeEach(() => {
pool = new DomElementObjectPool('span');
(<any>global).document = {
createElement: () => new MockDocument()
};
});
it('should acquire distinct elements', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
assert.notEqual(element1, element2);
});
it('should acquire released elements', () => {
const element = pool.acquire();
pool.release(element);
assert.equal(pool.acquire(), element);
});
it('should handle a series of acquisitions and releases', () => {
const element1 = pool.acquire();
const element2 = pool.acquire();
pool.release(element1);
assert.equal(pool.acquire(), element1);
pool.release(element1);
pool.release(element2);
assert.equal(pool.acquire(), element2);
assert.equal(pool.acquire(), element1);
});
it('should throw when releasing an element that was not acquired', () => {
assert.throws(() => pool.release(document.createElement('span')));
});
});
+75
View File
@@ -0,0 +1,75 @@
/**
* @module xterm/utils/DomElementObjectPool
* @license MIT
*/
/**
* An object pool that manages acquisition and releasing of DOM elements for
* when reuse is desirable.
*/
export class DomElementObjectPool {
private static readonly OBJECT_ID_ATTRIBUTE = 'data-obj-id';
private static _objectCount = 0;
private _type: string;
private _pool: HTMLElement[];
private _inUse: {[key: string]: HTMLElement};
/**
* @param type The DOM element type (div, span, etc.).
*/
constructor(private type: string) {
this._type = type;
this._pool = [];
this._inUse = {};
}
/**
* Acquire an element from the pool, creating it if the pool is empty.
*/
public acquire(): HTMLElement {
let element: HTMLElement;
if (this._pool.length === 0) {
element = this._createNew();
} else {
element = this._pool.pop();
}
this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)] = element;
return element;
}
/**
* Release an element back into the pool. It's up to the caller of this
* function to ensure that all external references to the element have been
* removed.
* @param element The element being released.
*/
public release(element: HTMLElement): void {
if (!this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)]) {
throw new Error('Could not release an element not yet acquired');
}
delete this._inUse[element.getAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE)];
this._cleanElement(element);
this._pool.push(element);
}
/**
* Creates a new element for the pool.
*/
private _createNew(): HTMLElement {
const element = document.createElement(this._type);
const id = DomElementObjectPool._objectCount++;
element.setAttribute(DomElementObjectPool.OBJECT_ID_ATTRIBUTE, id.toString(10));
return element;
}
/**
* Resets an element back to a "clean state".
* @param element The element to be cleaned.
*/
private _cleanElement(element: HTMLElement): void {
element.className = '';
element.innerHTML = '';
}
}
+81
View File
@@ -0,0 +1,81 @@
/**
* @license MIT
*/
import { CharMeasure } from './CharMeasure';
export function getCoordsRelativeToElement(event: MouseEvent, element: HTMLElement): [number, number] {
// Ignore browsers that don't support MouseEvent.pageX
if (event.pageX == null) {
return null;
}
let x = event.pageX;
let y = event.pageY;
// Converts the coordinates from being relative to the document to being
// relative to the terminal.
while (element && element !== self.document.documentElement) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
}
return [x, y];
}
/**
* Gets coordinates within the terminal for a particular mouse event. The result
* is returned as an array in the form [x, y] instead of an object as it's a
* little faster and this function is used in some low level code.
* @param event The mouse event.
* @param rowContainer The terminal's row container.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows n the terminal.
* @param isSelection Whether the request is for the selection or not. This will
* apply an offset to the x value such that the left half of the cell will
* select that cell and the right half will select the next cell.
*/
export function getCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
// Coordinates cannot be measured if charMeasure has not been initialized
if (!charMeasure.width || !charMeasure.height) {
return null;
}
const coords = getCoordsRelativeToElement(event, rowContainer);
if (!coords) {
return null;
}
// Convert to cols/rows.
coords[0] = Math.ceil((coords[0] + (isSelection ? charMeasure.width / 2 : 0)) / charMeasure.width);
coords[1] = Math.ceil(coords[1] / charMeasure.height);
// Ensure coordinates are within the terminal viewport.
coords[0] = Math.min(Math.max(coords[0], 1), colCount + 1);
coords[1] = Math.min(Math.max(coords[1], 1), rowCount + 1);
return coords;
}
/**
* Gets coordinates within the terminal for a particular mouse event, wrapping
* them to the bounds of the terminal and adding 32 to both the x and y values
* as expected by xterm.
* @param event The mouse event.
* @param rowContainer The terminal's row container.
* @param charMeasure The char measure object used to determine character sizes.
* @param colCount The number of columns in the terminal.
* @param rowCount The number of rows in the terminal.
*/
export function getRawByteCoords(event: MouseEvent, rowContainer: HTMLElement, charMeasure: CharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
const coords = getCoords(event, rowContainer, charMeasure, colCount, rowCount);
let x = coords[0];
let y = coords[1];
// xterm sends raw bytes and starts at 32 (SP) for each.
x += 32;
y += 32;
return { x, y };
}
+56 -37
View File
@@ -41,6 +41,9 @@
font-family: courier-new, courier, monospace;
font-feature-settings: "liga" 0;
position: relative;
user-select: none;
-ms-user-select: none;
-webkit-user-select: none;
}
.terminal.focus,
@@ -71,63 +74,55 @@
resize: none;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar) .terminal-cursor {
background-color: #fff;
color: #000;
.terminal a {
color: inherit;
text-decoration: none;
}
.terminal a:hover {
cursor: pointer;
text-decoration: underline;
}
.terminal a.xterm-invalid-link:hover {
cursor: text;
text-decoration: none;
}
.terminal .terminal-cursor {
position: relative;
}
.terminal:not(.focus) .terminal-cursor {
outline: 1px solid #fff;
outline-offset: -1px;
background-color: transparent;
}
.terminal:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus.xterm-cursor-blink .terminal-cursor {
animation: xterm-cursor-blink 1.2s infinite step-end;
.terminal.xterm-cursor-style-block.focus:not(.xterm-cursor-blink-on) .terminal-cursor {
background-color: #fff;
color: #000;
}
@keyframes xterm-cursor-blink {
0% {
background-color: #fff;
color: #000;
}
50% {
background-color: transparent;
color: #FFF;
}
}
.terminal.xterm-cursor-style-bar .terminal-cursor,
.terminal.xterm-cursor-style-underline .terminal-cursor {
position: relative;
}
.terminal.xterm-cursor-style-bar .terminal-cursor::before,
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
content: "";
display: block;
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before,
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
content: '';
position: absolute;
background-color: #fff;
}
.terminal.xterm-cursor-style-bar .terminal-cursor::before {
.terminal.focus.xterm-cursor-style-bar:not(.xterm-cursor-blink-on) .terminal-cursor::before {
top: 0;
bottom: 0;
left: 0;
bottom: 0;
width: 1px;
}
.terminal.xterm-cursor-style-underline .terminal-cursor::before {
.terminal.focus.xterm-cursor-style-underline:not(.xterm-cursor-blink-on) .terminal-cursor::before {
bottom: 0;
left: 0;
right: 0;
height: 1px;
}
.terminal.xterm-cursor-style-bar.focus.xterm-cursor-blink .terminal-cursor::before,
.terminal.xterm-cursor-style-underline.focus.xterm-cursor-blink .terminal-cursor::before {
animation: xterm-cursor-non-bar-blink 1.2s infinite step-end;
}
@keyframes xterm-cursor-non-bar-blink {
0% { background-color: #fff; }
50% { background-color: transparent; }
}
.terminal .composition-view {
background: #000;
@@ -148,7 +143,8 @@
overflow-y: scroll;
}
.terminal .xterm-wide-char {
.terminal .xterm-wide-char,
.terminal .xterm-normal-char {
display: inline-block;
}
@@ -174,6 +170,25 @@
left: -9999em;
}
.terminal.enable-mouse-events {
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
cursor: default;
}
.terminal .xterm-selection {
position: absolute;
top: 0;
left: 0;
z-index: 1;
opacity: 0.3;
pointer-events: none;
}
.terminal .xterm-selection div {
position: absolute;
background-color: #fff;
}
/*
* Determine default colors for xterm.js
*/
@@ -189,6 +204,10 @@
text-decoration: blink;
}
.terminal .xterm-blink.xterm-underline {
text-decoration: blink underline;
}
.terminal .xterm-hidden {
visibility: hidden;
}
+456 -245
View File
File diff suppressed because it is too large Load Diff
+1 -8
View File
@@ -12,13 +12,6 @@
"src/**/*"
],
"exclude": [
"src/addons/**/*",
"build",
"demo",
"dist",
"out",
"test",
"node_modules",
"docs"
"src/addons/**/*"
]
}