Compare commits

...
222 Commits
Author SHA1 Message Date
Paris Kasidiaris 1dbe7b3a8c Bump version to 3.1.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-02-09 08:42:39 +00:00
Daniel Imms 987be07a70 Merge pull request #1276 from Tyriar/enableBold_deprecated
Mark enableBold setting as deprecated
2018-02-07 07:46:50 -08:00
Daniel Imms 760837e5cb Mark enableBold setting as deprecated 2018-02-06 07:25:54 -08:00
Thomas Zilz dc759a93bc Support setting padding on the .xterm element (#1208)
* Support padding on the .xterm element
2018-02-06 12:31:17 +01:00
Daniel Imms 1ec7b91a98 Merge pull request #1264 from Tyriar/1021_support_modifiers
Allow support of modifiers with links
2018-02-06 02:32:00 -08:00
Daniel Imms c218eebb24 Merge pull request #1266 from Tyriar/1265_sync_when_attached
Ensure viewport is attached before syncing scrollbar
2018-02-06 02:24:59 -08:00
Daniel Imms 8040e3de4a Merge pull request #1275 from xtermjs/Tyriar-patch-1
Update readme copyright statement
2018-02-06 02:23:39 -08:00
Paris Kasidiaris ff400bfcdc Merge pull request #1273 from xtermjs/webpack-watch
Add `watch` gulp task and npm script
2018-02-06 12:20:36 +02:00
Daniel Imms 35ea593806 Update readme copyright statement 2018-02-06 02:07:56 -08:00
Paris Kasidiaris f02903a6df Update README.md and Docker files 2018-02-06 09:22:16 +00:00
Paris Kasidiaris 182426dd0f Rename gulp task: webpack:watch → watch` and also monitor subdirectories 2018-02-06 10:28:34 +02:00
Paris Kasidiaris d0f45fee25 Fix #1232: Add webpack:watch gulp task 2018-02-05 15:49:37 +00:00
Daniel Imms 1462b30c88 Merge pull request #1271 from krasimir/patch-1
Update README.md
2018-02-05 07:41:23 -08:00
Krasimir Tsonev c302bc9f17 Update README.md
In `Getting Started` section we read `npm install` only. Shouldn't be `npm install xterm`?
2018-02-05 09:32:34 +02:00
Daniel Imms 01d841fec6 Merge pull request #1246 from Tyriar/1245_revert_page_change
Revert "Add support for modifiers for PageUp / PageDown keys."
2018-02-02 09:51:28 -08:00
Daniel Imms 1124f1fc7f Merge pull request #1114 from nikonso/feature-778
Select the word underneath the cursor on contextmenu
2018-02-01 17:35:52 -08:00
Daniel Imms cab097c09c Clean up condition for readability 2018-02-01 17:28:08 -08:00
Daniel Imms 43adf0ef70 Merge branch 'master' into feature-778 2018-02-01 17:21:04 -08:00
Daniel Imms 3c6cab1491 Clear selectionEnd when selecting word at cursor 2018-02-01 17:20:33 -08:00
Bruno Ribeito 5550bfd90b Fix is click in selection 2018-02-01 21:37:06 +00:00
Daniel Imms 04ccb638b4 Fix lint 2018-02-01 11:28:56 -08:00
Daniel Imms 5c05471993 Merge branch 'master' into 1021_support_modifiers 2018-02-01 11:28:05 -08:00
Daniel Imms 1e32d26a85 Merge branch 'master' into 1265_sync_when_attached 2018-02-01 10:28:41 -08:00
Daniel Imms 6105975308 Ensure viewport is attached before syncing scrollbar
Fixes #1265
2018-02-01 10:24:17 -08:00
Daniel Imms cd24f7a26c Merge pull request #1263 from Tyriar/ts_2.7
typescript@2.7
2018-02-01 08:16:42 -08:00
Daniel Imms 2ac5a8da49 Allow support of modifiers with links
Fixes #1021
2018-01-31 17:42:33 -08:00
Daniel Imms e849629533 typescript@2.7
Tuples using a fixed length by default will be helpful for CharData
2018-01-31 11:32:19 -08:00
Bruno Ribeito 983af8fac2 Use _selectWordA 2018-01-31 00:01:46 +00:00
Daniel Imms 62d6c242fc Merge pull request #1249 from Tyriar/1248_tslint_curly
Add curly to tslint, fix errors
2018-01-29 07:54:09 -08:00
Daniel Imms e7b4e002f4 Merge remote-tracking branch 'origin/master' into 1248_tslint_curly 2018-01-29 07:45:31 -08:00
Daniel Imms d190bc0531 Merge pull request #1250 from xtermjs/Tyriar-patch-1
Add SQL Operations Studio to real world uses
2018-01-29 07:44:36 -08:00
Daniel Imms fb1d902804 Merge pull request #1258 from Tyriar/travis_tsc
Run tsc as a travis job
2018-01-29 07:44:26 -08:00
Daniel Imms 12cd6ea0e4 Merge pull request #1260 from Tyriar/typo
Fix recent typo in comment
2018-01-29 07:43:56 -08:00
Daniel Imms 69ebb3e53e Fix recent typo in comment 2018-01-29 07:42:59 -08:00
Paris Kasidiaris f2fe8ca08b Merge pull request #896 from npezza93/alt-click
Alt+click will move the prompt cursor to that position
2018-01-29 11:58:46 +02:00
Daniel Imms bbac1801e5 Small clean up 2018-01-28 17:33:51 -08:00
Daniel Imms a6a4bf0817 Merge remote-tracking branch 'ups/master' into feature-778 2018-01-28 17:25:46 -08:00
Daniel Imms e8666cafc9 Small clean up 2018-01-28 14:44:33 -08:00
Daniel Imms db95442af6 Merge pull request #1236 from chabou/pr/1148
Add allowTransparency option
2018-01-28 14:19:20 -08:00
Daniel Imms fc2af1d806 Fix lint, reduce duplication, don't scale ctx
Scaling ctx was breaking the scale when setOption was called, not sure
it was doing anything.
2018-01-28 14:13:25 -08:00
Daniel Imms 106704b5d8 Merge remote-tracking branch 'ups/master' into pr/1148 2018-01-28 13:39:07 -08:00
Daniel Imms af375eadb4 Run tsc as a travis job
This will hard fail when there are semantic errors. Currently building does
not do this and we will merge these in as a result.
2018-01-28 12:27:01 -08:00
Daniel Imms 098a3d0804 Merge pull request #1253 from sitzmar/right_click_timeout
adjust timeout to ensure right-click event has fired
2018-01-28 10:35:44 -08:00
npezza93 3bdc069f19 Set copyright header to 2018 2018-01-28 13:16:20 -05:00
npezza93 3313716c87 Add new lines back in types 2018-01-28 13:15:42 -05:00
npezza93 735ffdd2ce Merge remote-tracking branch 'upstream/master' into alt-click 2018-01-28 13:13:37 -05:00
Daniel Imms 8588199fdb Merge pull request #1256 from Tyriar/1255_id_class
Change render layer id to a class
2018-01-28 10:13:13 -08:00
npezza93 f726c1fd0d Allow selection and alt click to live conflict free 2018-01-28 13:06:54 -05:00
npezza93 e60704255f Fix lints 2018-01-28 13:00:11 -05:00
npezza93 aeae40e110 Use the terminal and circular list interface 2018-01-28 12:38:20 -05:00
npezza93 08af2ebeea Utilize enum to simplify sequence generation 2018-01-28 12:29:52 -05:00
npezza93 0202659c38 Set terminal and mouseevent inside constructor args 2018-01-28 10:48:08 -05:00
npezza93 edadb6e0f1 Update the copyright header 2018-01-28 10:44:01 -05:00
Daniel Imms 39abf8aecf Change render layer id to a class
It's only used to identify them in DOM explorer currently

Fixes #1255
2018-01-27 22:26:00 -08:00
Daniel Imms 65b6901170 Fix compile error 2018-01-27 21:51:13 -08:00
npezza93 14725d0b0d Merge remote-tracking branch 'upstream/master' into alt-click 2018-01-27 22:38:12 -05:00
npezza93 c41a4b5168 Move repeat import 2018-01-27 22:37:57 -05:00
npezza93 d61aa6e470 Fix lints 2018-01-27 22:37:31 -05:00
npezza93 544fa67851 Move repeat out of generic 2018-01-27 22:36:53 -05:00
npezza93 6a48022a45 Don't reset starting position if the row is not moved.
Mainly due to the normal buffer
2018-01-27 22:28:41 -05:00
npezza93 a4ee723665 Fix backward row/col definition 2018-01-27 22:06:58 -05:00
npezza93 dc59f33ccf Fix translateBufferToString bug 2018-01-27 22:06:31 -05:00
npezza93 833d814499 Refactor alt-click to work independently of which buffer is being used 2018-01-27 21:30:53 -05:00
npezza93 327b16ac2e Add repeat polyfill for repeating a string 2018-01-27 21:30:24 -05:00
Daniel Imms 9feb10efc5 Merge pull request #1244 from Tyriar/remove_legacy_gitignore
Remove legacy .gitignore rules
2018-01-26 10:34:43 -08:00
Daniel Imms 58f3793cd4 Merge branch 'master' into remove_legacy_gitignore 2018-01-26 10:27:44 -08:00
Daniel Imms 93715c455d Add dist/ back 2018-01-26 10:26:31 -08:00
sitzmar 069b489fb6 adjust timeout to ensure right-click event has fired 2018-01-26 08:04:59 -08:00
CHaBou fe53e1a091 Change canvas transparency when option is updated 2018-01-25 23:20:20 +01:00
Bruno Ribeito e6c6e0766a Fix usage of allowWhitespaceOnlySelection variable 2018-01-25 21:00:10 +00:00
Daniel Imms 2c065b66be Add SQL Operations Studio to real world uses 2018-01-25 12:31:48 -08:00
Daniel Imms 1dce76dbbb Add curly to tslint, fix errors
Fixes #1248
2018-01-25 12:17:59 -08:00
Daniel Imms 9c62a817d0 Fix build after merge 2018-01-25 12:08:50 -08:00
Daniel Imms 3a3f3db56e Merge remote-tracking branch 'ups/master' into feature-778 2018-01-25 11:58:46 -08:00
Daniel Imms bd55ce841e Merge branch 'master' into 1245_revert_page_change 2018-01-25 11:28:29 -08:00
Daniel Imms 6c54c6cded Merge pull request #1224 from Tyriar/1221_iterminal_typings
Move ITerminal and all its dependents into typings/
2018-01-25 11:25:02 -08:00
Daniel Imms c12394a687 Don't export contains 2018-01-25 11:09:20 -08:00
Daniel Imms 2e23dd34e0 Move xterm-internal types back into Types.ts files 2018-01-25 11:06:52 -08:00
Daniel Imms 8225b94d34 Types.d.ts -> .ts 2018-01-25 10:24:17 -08:00
Daniel Imms 4db4fcb40a Remove declaration file generation, we have explicit types 2018-01-25 10:23:14 -08:00
Daniel Imms 177edfa66e Merge interfaces and types files 2018-01-25 10:03:28 -08:00
Daniel Imms d454a50a84 Move FontWeight type to xterm.d.ts 2018-01-25 09:48:12 -08:00
Daniel Imms 7358890a31 Merge remote-tracking branch 'ups/master' into 1221_iterminal_typings 2018-01-25 09:45:13 -08:00
Daniel Imms 4c6f18c36f Revert "Add support for modifiers for PageUp / PageDown keys."
This reverts commit 83921c0b26.

Fixes #1245
2018-01-25 09:20:19 -08:00
Daniel Imms 70747e84a1 Merge remote-tracking branch 'ups/master' into pr/1148 2018-01-25 08:15:42 -08:00
Daniel Imms 8f1a629b48 Merge pull request #1198 from Tyriar/955_worker_refactor
Refactor char atlas to enable generation with worker
2018-01-25 07:22:15 -08:00
Daniel Imms b87fe53b94 Remove legacy .gitignore rules 2018-01-25 07:20:41 -08:00
Daniel Imms a22b5c0faa Merge remote-tracking branch 'ups/master' into 955_worker_refactor 2018-01-25 07:16:00 -08:00
Daniel Imms 92e85e0ce5 Merge pull request #1228 from Tyriar/missing_typings
Add missing macOptionIsMeta typings
2018-01-25 07:00:08 -08:00
Daniel Imms 038260b92a Merge pull request #1235 from Tyriar/1151_clamp_saved_cursor
Resize saved cursor position on buffer resize
2018-01-25 06:55:02 -08:00
Daniel Imms d17c08a32f Merge pull request #1238 from Tyriar/1117_reenable_enableBold
Add enableBold back, add fontWeight types to xterm.d.ts
2018-01-25 06:54:45 -08:00
Daniel Imms c1f18c33cc Merge pull request #1192 from Tyriar/master_version
Set version to 3.1.0-master
2018-01-25 06:54:25 -08:00
Daniel Imms 3798b28aa2 Merge pull request #1211 from Tyriar/1103_search_wide_emoji
Fix search addon for lines with emojis and wide chars
2018-01-24 07:52:29 -08:00
Daniel Imms e539a59e26 Merge branch 'master' into 1103_search_wide_emoji 2018-01-24 07:33:08 -08:00
npezza93 027bc9535e Pass false for isSelection so the right half of the cell wont select the next element 2018-01-23 20:57:31 -05:00
npezza93 8d41c4c603 Fix lints 2018-01-23 20:12:39 -05:00
npezza93 7e0a484000 Handle going back and up in the alt buffer from a long line to a short line 2018-01-23 20:12:39 -05:00
npezza93 14498104fc Alt+click will move the prompt cursor to that position
Fixes #890
2018-01-23 20:12:39 -05:00
Daniel Imms a74f7ba196 Merge branch 'master' into 1151_clamp_saved_cursor 2018-01-23 10:48:54 -08:00
Daniel Imms 46f35064e3 Merge pull request #1227 from Tyriar/typo_intefaces
Fix Intefaces typo
2018-01-23 09:07:05 -08:00
Daniel Imms 8f263fb987 Merge branch 'master' into typo_intefaces 2018-01-23 09:06:47 -08:00
Daniel Imms f56f4520d0 Merge branch 'master' into 1103_search_wide_emoji 2018-01-23 09:06:06 -08:00
Daniel Imms a13562bf45 Remove relative font weight as valid values 2018-01-23 08:58:33 -08:00
Daniel Imms 067acd8803 Merge branch 'master' into pr/1148 2018-01-23 08:33:55 -08:00
Daniel Imms df02dacdc4 Add enableBold back
Part of #1117
2018-01-23 08:28:01 -08:00
Daniel Imms 4e6b089216 Merge pull request #1119 from nikonso/1117
Support setting font weight
2018-01-23 08:11:27 -08:00
Daniel Imms 01c95168f2 Make font weight types in ITerminalOption more strict 2018-01-23 08:02:48 -08:00
Daniel Imms f3ecbea3e8 Fix tests 2018-01-23 07:55:18 -08:00
Daniel Imms 11aa193074 Merge branch 'master' into 1117 2018-01-23 07:51:25 -08:00
Daniel Imms 2ebc5d904d Resize saved cursor position on buffer resize
Saved cursor coordinates were not being resized as well which could
lead to crashes when resizing down while the alt buffer is active.

Fixes #1151
2018-01-23 06:51:22 -08:00
Daniel Imms d85d353772 Merge pull request #1234 from Tyriar/1233_link_activation
Use explicit null check to ensure start/end row is valid
2018-01-22 12:53:00 -08:00
Daniel Imms a9d3e61794 Fix tests 2018-01-22 12:43:42 -08:00
Daniel Imms 16abf71b78 Use explicit null check to ensure start/end row is valid
It looks like this could have caused the inconsistent behavior with
links not activating and then working again after scrolling

Fixes #1233
2018-01-22 11:51:09 -08:00
Daniel Imms f554f53bda Merge pull request #1229 from ColdGrub1384/patch-1
Include Pisth in applications
2018-01-21 10:22:42 -08:00
Daniel Imms 49565c2fcf Merge branch 'master' into patch-1 2018-01-21 10:22:19 -08:00
Daniel Imms a338def801 Merge pull request #1230 from jkuri/abstruse-readme
Add AbstruseCI as real-world example
2018-01-21 10:16:16 -08:00
Jan Kuri a9a1d37a1e Add AbstruseCI as real-world example 2018-01-21 18:49:10 +01:00
Adrian Labbé e55a234b53 Include Pisth in applications 2018-01-20 16:07:31 -03:00
Daniel Imms 4e20b48aa0 Add missing macOptionIsMeta typings
Follow up from #1225
2018-01-20 11:06:53 -08:00
Daniel Imms 616958e2c6 Merge pull request #1225 from saamalik/altIsMeta
Mac OSX: altIsMeta support
2018-01-20 10:15:03 -08:00
Daniel Imms e4726192f1 Various fixes
- altIsMeta -> macOptionIsMeta
- Moved isThirdLevelShift into Terminal class
- Moved option check into isThirdLevelShift so it doesn't affect the Windows
  case and also includes the keypress case
- Add public API and typings test
- Add option checkbox to demo
2018-01-20 09:06:05 -08:00
Daniel Imms 4f59363331 Fix Intefaces typo 2018-01-20 08:22:12 -08:00
Saad Malik 73686e66c6 Remove package-lock changes 2018-01-19 16:51:57 -08:00
Saad Malik d981a9fe6c Merge branch 'master' into altIsMeta 2018-01-19 12:49:07 -08:00
Daniel Imms 903ec3c910 Merge pull request #1206 from amejia1/master
Fix issue with resizing terminal.
2018-01-19 12:40:18 -08:00
Daniel Imms eb6ff003da Merge branch 'master' into master 2018-01-19 12:34:43 -08:00
Daniel Imms 6e3cd2cb23 Fix indentation 2018-01-19 12:34:32 -08:00
Saad Malik ec8593fe46 Merge branch 'master' into altIsMeta 2018-01-19 12:32:06 -08:00
Saad Malik 891fbd6b80 Mac OSX: altIsMeta support
* Support for altIsMeta option
* Instead of providing third-level shift characters
* Sample demo application using new altIsMeta option
2018-01-19 12:10:39 -08:00
Daniel Imms e4abd4dd8a Move Shared interfaces into a new typings file
This will enable sharing the internal interface with addons (and catching
breakages early)
2018-01-19 09:48:33 -08:00
Daniel Imms 78a63d4860 Simplify interfaces, reduce duplication 2018-01-19 09:48:33 -08:00
Daniel Imms 47df04976c Base ITerminal on interface in .d.ts
Part of #1221
2018-01-19 09:48:33 -08:00
Daniel Imms d33c340de5 Merge pull request #1222 from Tyriar/1220_addon_types
Use Terminal types from xterm.d.ts in Addons
2018-01-19 09:34:44 -08:00
Daniel Imms 57a9948e8d Use Terminal typings in remaining addons
Fixes #1220
2018-01-19 09:30:24 -08:00
Daniel Imms ec17fb9179 Use Terminal type in attach, fit and fullscreen 2018-01-19 09:30:24 -08:00
Daniel Imms 682eb2f861 Use Terminal public API type in search addon
Part of #1220
2018-01-19 09:30:24 -08:00
Daniel Imms a9f69965fe Merge pull request #1223 from zhaojh329/master
Add rtty to the real-world uses section of README
2018-01-18 21:40:55 -08:00
Jianhui Zhao 1f79860aaf Add rtty to the real-world uses section of README
A reverse proxy WebTTY. It is composed of the client and the server.

Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
2018-01-19 10:57:51 +08:00
Andres Mejia 5133f25fb7 Fix issue with resetting terminal.
Ensure new BufferSet is used and clear selection manager on reset.
2018-01-18 17:48:29 -05:00
Daniel Imms cfd00529eb Various fixes
- Add buffers being retained on full reset (CIS)
- Remove setup event, simplify
- Only have one trim listener active at once
- Disallow activate events if nothing changed (just in case)
2018-01-18 17:48:29 -05:00
Andres Mejia a9b7ef5dc3 Clear selection manager when the terminal switches its buffer set or buffer. 2018-01-18 17:48:29 -05:00
Andres Mejia 90c8c85c09 Fix issue with resizing terminal.
Need to ensure the active buffer is always being used.
Fixes https://github.com/xtermjs/xterm.js/issues/824 .
2018-01-18 17:48:28 -05:00
Bruno Ribeiro 9f8098c55b Merge branch 'master' into feature-778 2018-01-18 21:10:50 +00:00
Bruno Ribeiro b8f2832ed4 Merge branch 'master' into 1117 2018-01-18 21:10:09 +00:00
Daniel Imms 2f5029205d Merge pull request #1214 from Tyriar/fix_lint
Fix lint, update tslint and add more tslint rules
2018-01-18 12:28:02 -08:00
Daniel Imms 0645fe582f Fix missing space 2018-01-18 12:27:07 -08:00
Daniel Imms 9c4ef2fd86 Merge remote-tracking branch 'origin/master' into fix_lint 2018-01-18 12:11:21 -08:00
Daniel Imms 8d5ed6fc75 Merge pull request #1197 from blink1073/fix-typings
Ship the declaration files
2018-01-18 12:03:33 -08:00
Daniel Imms 1b48f2cd6d Merge branch 'master' into fix-typings 2018-01-18 11:56:10 -08:00
Daniel Imms 15dd1e6bec Merge pull request #1217 from Tyriar/1216_invalidate_cursor
Fix cursor lagging behind when blink is on
2018-01-18 10:54:26 -08:00
Daniel Imms b4b4456225 Merge branch 'master' into 1216_invalidate_cursor 2018-01-18 10:54:18 -08:00
Daniel Imms b89f9666e6 Merge pull request #1218 from Tyriar/travis_osx
Remove OSX from travis
2018-01-18 10:54:02 -08:00
Daniel Imms 2253e666e7 Remove OSX from travis
Builds take too long on free tier
2018-01-18 09:39:32 -08:00
Daniel Imms 777603b5cd Merge branch 'master' into 1216_invalidate_cursor 2018-01-18 09:01:27 -08:00
Daniel Imms 0379ef7bc7 Merge pull request #1215 from markus-oberhumer/ctrl-pageup-pagedown
Add support for modifiers for PageUp / PageDown keys.
2018-01-18 07:58:31 -08:00
Daniel Imms b1beee3595 Fix cursor lagging behind when blink is on
Fixes #1216
2018-01-18 07:54:45 -08:00
Markus F.X.J. Oberhumer 83921c0b26 Add support for modifiers for PageUp / PageDown keys.
This allows using Ctrl-PageUp/Down and Alt-PageUp/Down.
2018-01-18 09:23:05 +01:00
Daniel Imms 4588d15738 Add more tslint rules, fix attach addon 2018-01-17 10:40:42 -08:00
Daniel Imms 307dd0fd68 Add several tslint rules 2018-01-17 10:32:35 -08:00
Daniel Imms 53e1f83bb5 Update tslint, fix lint 2018-01-17 10:23:27 -08:00
Daniel Imms 70236f6818 Fix addon lint 2018-01-17 10:20:04 -08:00
Daniel Imms 5888cd7938 Merge pull request #1210 from Tyriar/1194_initial_cursorBlink
Ensure renderer uses latest options at open
2018-01-16 09:21:39 -08:00
Daniel Imms 0e870d106f Fix search for emojis
Fixes #1103
2018-01-16 07:26:03 -08:00
Daniel Imms 8d3bdad7e4 Fix search for wide unicode
Part of #1103
2018-01-16 07:06:10 -08:00
Daniel Imms f02ead309b Merge branch 'master' into 1194_initial_cursorBlink 2018-01-16 06:45:58 -08:00
Daniel Imms 80f13792a3 Ensure renderer uses latest options at open
Fixes #1194
2018-01-16 06:34:22 -08:00
Paris Kasidiaris 691f8a7567 Merge pull request #1199 from ficristo/umd-addons
Make addons UMD compatible
2018-01-15 22:11:21 +02:00
ficristo 46f684621c Merge branch 'master' into umd-addons 2018-01-14 20:24:52 +01:00
Daniel Imms adcbb1ae3f Merge pull request #1207 from amejia1/new-use-case
Add use case for atom-xterm.
2018-01-13 20:55:41 -08:00
Andres Mejia a7101b5bcb Add use case for atom-xterm. 2018-01-13 15:34:05 -05:00
Daniel Imms cc80114748 Merge pull request #1205 from jluk/patch-1
Include cloud shell in xterm applications
2018-01-12 17:41:08 -08:00
Justin Luk 5a147f201f Include cloud shell in xterm applications 2018-01-12 15:13:19 -08:00
ficristo b7c0ca11fe Make addons UMD compatible 2018-01-07 09:02:41 +01:00
Daniel Imms e49d844284 Add note in README about code structure 2018-01-06 13:46:27 -08:00
Daniel Imms d0ef868215 Refactor char atlas to enable generation with worker
This doesn't actually add workers but it separates the code nicely
such that the minimal amount of relevant code can be pulled in by
worker code.

Part of #955
2018-01-06 13:43:34 -08:00
Steven Silvester a36a17928d Ship the declaration files 2018-01-06 07:18:07 -06:00
Daniel Imms 50ed83ca64 Set version to 3.1.0-master
This will make it clear in bug reports etc. when someone is running off
master and not 3.0.
2018-01-05 14:40:41 -08:00
Daniel Imms be18507df6 Merge pull request #1172 from Tyriar/1118_dpi_change
Update charatlas when monitor DPI changes
2018-01-05 14:24:12 -08:00
Daniel Imms f6da87f3b4 Merge remote-tracking branch 'origin/master' into 1118_dpi_change 2018-01-05 14:19:21 -08:00
Daniel Imms 248fbccfd9 Remove console.log 2018-01-05 14:18:16 -08:00
Daniel Imms eac14a149f Merge pull request #1144 from Tyriar/880_renderer_pause
Use IntersectionObserver to turn off renderer when hidden
2018-01-05 14:16:10 -08:00
Daniel Imms d90f48b210 Merge branch 'master' into 880_renderer_pause 2018-01-05 14:15:26 -08:00
Daniel Imms eb1110e3f5 Merge pull request #1191 from astefanutti/kubebox
Add Kubebox to real-world uses
2018-01-05 13:35:59 -08:00
Antonin Stefanutti 82e7fe5a1b Add Kubebox to real-world uses 2018-01-05 22:12:32 +01:00
Daniel Imms 1bf084921e Merge branch 'master' into 1117 2018-01-05 11:03:50 -08:00
Daniel Imms 25972d1e06 Merge branch 'master' into feature-778 2018-01-05 11:03:33 -08:00
Daniel Imms b3c8923709 Merge branch 'master' into 1117 2018-01-05 11:03:24 -08:00
Daniel Imms 13d1020308 Merge pull request #1137 from damz/pr/parser-escape-state
Fix parser not resetting state for some escape sequences
2018-01-05 11:02:24 -08:00
Daniel Imms b98f10625f Merge branch 'master' into pr/parser-escape-state 2018-01-05 11:02:14 -08:00
Daniel Imms 09605dba16 Merge pull request #1142 from Tyriar/1067_fix_cursor_on_right
Fix cursor on scroll bar
2018-01-05 11:01:30 -08:00
Daniel Imms 1509f88dbf Merge branch 'master' into 1067_fix_cursor_on_right 2018-01-05 11:01:21 -08:00
Daniel Imms 884c7c631d Merge branch 'master' into 880_renderer_pause 2018-01-05 11:01:06 -08:00
Daniel Imms b66578a4ca Merge branch 'master' into 880_renderer_pause 2018-01-05 11:00:11 -08:00
Daniel Imms b0b6c8d878 Merge branch 'master' into transparency 2018-01-05 10:58:59 -08:00
Daniel Imms 9f1f86c316 Merge pull request #1168 from Tyriar/1152_npe_selection_manager_2
Prevent dragging from setting selection y over bounds
2018-01-05 10:58:40 -08:00
Daniel Imms 80a1c2bd50 Merge branch 'master' into 1152_npe_selection_manager_2 2018-01-05 10:58:22 -08:00
Daniel Imms 09139cc350 Merge pull request #1177 from electerm/add-electerm-use-case
add Real-world use: electerm
2018-01-05 10:56:08 -08:00
Daniel Imms 019eb30b68 Merge pull request #1180 from Tyriar/1179_alt_backspace
Allow alt+backspace to delete a word
2018-01-05 10:55:57 -08:00
Daniel Imms 58f80c404f Merge branch 'master' into add-electerm-use-case 2018-01-05 09:14:37 -08:00
Daniel Imms 22f860536d Merge branch 'master' into add-electerm-use-case 2018-01-05 09:13:55 -08:00
Daniel Imms e9ec1bda3d Merge branch 'master' into 1179_alt_backspace 2018-01-05 09:13:32 -08:00
Daniel Imms 9a743ef301 Merge pull request #1188 from xtermjs/Tyriar-patch-1
Remove repo redirect from package.json
2018-01-05 09:12:44 -08:00
Daniel Imms 0de2d3846a Merge branch 'master' into Tyriar-patch-1 2018-01-05 09:12:30 -08:00
Daniel Imms 0dd9b8d025 Remove repo redirect from package.json 2018-01-05 08:20:40 -08:00
Daniel Imms 0819759814 Allow alt+backspace to delete a word
Fixes #1179
2018-01-02 10:33:17 -08:00
Daniel Imms ecc1055c10 Listen to resolution media query 2018-01-02 10:11:24 -08:00
ZHAO Xudong ac1023dac6 add Real-world use: electerm 2018-01-02 14:45:43 +08:00
Daniel Imms 72d946e089 Use a more robust solution to changing devicePixelRatio 2017-12-29 13:54:01 -08:00
Daniel Imms ef5beb6706 Update charatlas when monitor DPI changes
Fixes #1118
2017-12-29 08:30:13 -08:00
Daniel Imms cf2116ff6a Prevent dragging from setting selection y over bounds
Fixes #1152
2017-12-27 08:08:28 -08:00
CHaBou 9c5aaed441 Fix some function parameters 2017-12-15 00:02:37 +01:00
CHaBou 9f85790ad1 Add allowTransparency option
Fixes #1004
2017-12-14 22:55:01 +01:00
Daniel Imms 47a50acc55 Use IntersectionObserver to turn off renderer when hidden
Fixes #880
2017-12-11 15:16:13 -08:00
Daniel Imms 616f0f9d36 Fix cursor on scroll bar
Fixes #1067
2017-12-11 10:13:17 -08:00
Damien Tournoud 981da3d9e6 Fix parser not resetting state for some escape sequences 2017-12-02 20:45:23 -08:00
Bruno Ribeito 6822070222 Fix typo + Cleanup logic 2017-11-12 23:15:09 +00:00
Bruno Ribeito 51de968fde Fix non-white colors not being drawn as bold 2017-11-12 23:05:27 +00:00
Bruno Ribeito f12eb5367b Adress feedback 2017-11-12 22:04:10 +00:00
Bruno Ribeito 007410995a Fix#1117 2017-11-12 22:04:10 +00:00
Bruno Ribeito 18b86a1cbc Do the selection even if there is already a selection 2017-11-08 23:27:00 +00:00
Bruno Ribeito 2865d935b6 Adress feedback 2017-11-07 21:21:44 +00:00
Bruno Ribeito 1877e7019d Implement #778 2017-11-06 23:20:57 +00:00
89 changed files with 5875 additions and 2012 deletions
+1 -5
View File
@@ -19,10 +19,6 @@ fixtures/typings-test/*.js
# Directories needed for code coverage
/coverage/
# Keep legacy files out of the repo, this can be removed when we merge v3 into master
# Keep bundled code out of Git
dist/
src/utils/TestUtils.ts
src/xterm.js
# Keep the demo builds out of Git
demo/dist/
+1 -1
View File
@@ -1,7 +1,6 @@
language: node_js
os:
- linux
- osx
node_js:
- 6
before_install:
@@ -12,6 +11,7 @@ before_install:
- npm install -g npm@5.1.0
env:
matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint
- NPM_COMMAND=test
notifications:
+13
View File
@@ -1,12 +1,15 @@
List of xterm.js contributors. Updated before every release.
Adrian Labbé <adrilabbelol@gmail.com>
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>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
@@ -19,6 +22,7 @@ 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>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
@@ -26,6 +30,7 @@ Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
@@ -46,20 +51,25 @@ imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
@@ -75,10 +85,12 @@ Paris Kasidiaris <paris@sourcelair.com>
Peter Baumgarten <me@peterbaumgarten.com>
Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
t-amqi <umberos@gmail.com>
@@ -90,3 +102,4 @@ Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:6
FROM node:8
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
# Set the working directory
+1
View File
@@ -1 +1,2 @@
web: npm start
webpack: npm run watch
+54 -9
View File
@@ -24,7 +24,7 @@ It enables applications to provide fully featured terminals to their users and c
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
```
npm install
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
@@ -118,32 +118,73 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom.
- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017.
- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React.
- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm.
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
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.
## Demo
### Linux or macOS
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run these commands:
Below you can find instructions on how to run the demo on different platforms.
### SourceLair
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
```
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
### Foreman (or other Procfile runner)
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
```
Then open http://0.0.0.0:3000 in a web browser.
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Linux or macOS
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
```
npm install
npm start
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://127.0.0.1:3000 in a web browser.
Then open http://127.0.0.1:3000 in a web browser to access the demo.
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
@@ -159,10 +200,14 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex
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.
### Code structure
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker.
## 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) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (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)
+8 -1
View File
@@ -23,6 +23,9 @@
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
</p>
<p>
<label>
cursorStyle
@@ -61,10 +64,14 @@
<label for="rows">Rows</label>
<input type="number" id="rows" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
</div>
</div>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+16 -6
View File
@@ -27,28 +27,33 @@ var terminalContainer = document.getElementById('terminal-container'),
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
scrollback: document.querySelector('#option-scrollback'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
rowsElement = document.getElementById('rows'),
paddingElement = document.getElementById('padding');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.renderer.dimensions.actualCellWidth + 20 /*room for scrollbar*/).toString() + 'px';
var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.resize(cols, rows);
term.fit();
}
function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
}
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
@@ -72,6 +77,9 @@ optionElements.cursorStyle.addEventListener('change', function () {
optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value);
});
optionElements.macOptionIsMeta.addEventListener('change', function () {
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
@@ -87,6 +95,7 @@ function createTerminal() {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
@@ -114,6 +123,7 @@ function createTerminal() {
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
+7
View File
@@ -7,3 +7,10 @@ services:
- ./:/usr/src/app
ports:
- ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"]
watch:
build: .
volumes:
- ./:/usr/src/app
command: ["npm", "run", "watch"]
+12
View File
@@ -144,6 +144,11 @@ namespace methods_core {
const r20: string = t.getOption('bellStyle');
const r21: boolean = t.getOption('enableBold');
const r22: number = t.getOption('letterSpacing');
const r23: boolean = t.getOption('macOptionIsMeta');
const r24: string = t.getOption('fontWeight');
const r25: string = t.getOption('fontWeightBold');
const r26: boolean = t.getOption('allowTransparency');
const r27: boolean = t.getOption('rightClickSelectsWord');
}
{
const t: Terminal = new Terminal();
@@ -157,9 +162,14 @@ namespace methods_core {
t.setOption('debug', true);
t.setOption('disableStdin', true);
t.setOption('enableBold', true);
t.setOption('fontWeight', 'normal');
t.setOption('fontWeight', 'bold');
t.setOption('fontWeightBold', 'normal');
t.setOption('fontWeightBold', 'bold');
t.setOption('popOnBell', true);
t.setOption('screenKeys', true);
t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true);
t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1);
@@ -177,6 +187,8 @@ namespace methods_core {
t.setOption('lineHeight', 1);
t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'});
t.setOption('macOptionIsMeta', true);
t.setOption('rightClickSelectsWord', false);
}
}
namespace scrolling {
+42 -46
View File
@@ -18,11 +18,13 @@ const ts = require('gulp-typescript');
const util = require('gulp-util');
const webpack = require('webpack-stream');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
// Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute.
if (path.normalize(outDir).indexOf(__dirname) !== 0) {
@@ -45,15 +47,19 @@ gulp.task('tsc', function () {
tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir))
);
let addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
let addonStreams = addons.map(function(addon) {
fs.emptyDirSync(`${outDir}/addons/${addon}`);
let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`);
let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon());
let tscAddon = tsResultAddon.js
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`));
let tscAddon = merge(
tsResultAddon.js
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`)),
tsResultAddon.dts
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`))
)
return tscAddon;
});
@@ -103,45 +109,29 @@ gulp.task('browserify', ['tsc'], function() {
});
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)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
const bundles = addons.map((addon) => {
const addonOptions = {
basedir: `${buildDir}/addons/${addon}`,
debug: true,
entries: [`${outDir}/addons/${addon}/${addon}.js`],
standalone: addon,
cache: {},
packageCache: {}
};
let winptyCompatOptions = {
basedir: `${buildDir}/addons/winptyCompat`,
debug: true,
entries: [`${outDir}/addons/winptyCompat/winptyCompat.js`],
cache: {},
packageCache: {}
};
let winptyCompatBundle = browserify(winptyCompatOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/winptyCompat/winptyCompat.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
const addonBundle = browserify(addonOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source(`./addons/${addon}/${addon}.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/**/*`
]).pipe(gulp.dest(`${buildDir}/addons`));
return addonBundle;
});
return merge(searchBundle, winptyCompatBundle, copyAddons);
return merge(...bundles);
});
gulp.task('instrument-test', function () {
@@ -189,9 +179,11 @@ gulp.task('sorcery', ['browserify'], function () {
});
gulp.task('sorcery-addons', ['browserify-addons'], function () {
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
chain.apply();
chain.writeSync();
addons.forEach((addon) => {
const chain = sorcery.loadSync(`${buildDir}/addons/${addon}/${addon}.js`);
chain.apply();
chain.writeSync();
})
});
gulp.task('webpack', ['build'], function() {
@@ -200,6 +192,10 @@ gulp.task('webpack', ['build'], function() {
.pipe(gulp.dest('demo/dist/'));
});
gulp.task('watch', ['webpack'], () => {
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
});
/**
* Submit coverage results to coveralls.io
*/
+3718 -708
View File
File diff suppressed because it is too large Load Diff
+7 -6
View File
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.0.0",
"version": "3.1.0",
"ignore": [
"demo",
"test",
@@ -9,7 +9,7 @@
],
"main": "lib/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/sourcelair/xterm.js",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"files": [
"*.js",
@@ -66,8 +66,8 @@
"node-pty": "^0.7.2",
"nodemon": "1.10.2",
"sorcery": "^0.10.0",
"tslint": "^4.0.2",
"typescript": "~2.4.0",
"tslint": "^5.9.1",
"typescript": "~2.7.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
@@ -75,17 +75,18 @@
"zmodem.js": "^0.1.5"
},
"scripts": {
"prestart": "gulp webpack",
"start": "node demo/app",
"prestart-zmodem": "npm run build",
"start-zmodem": "node build/addons/zmodem/demo/app",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc",
"build": "gulp build",
"prepublish": "npm run build",
"coveralls": "gulp coveralls",
"webpack": "gulp webpack"
"webpack": "gulp webpack",
"watch": "gulp watch"
},
"dependencies": {}
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
+9 -15
View File
@@ -3,9 +3,8 @@
* @license MIT
*/
import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList';
import { LineData, CharData } from './Types';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
@@ -121,11 +120,6 @@ export class Buffer implements IBuffer {
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
}
@@ -182,16 +176,13 @@ export class Buffer implements IBuffer {
}
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
this.x = Math.min(this.x, newCols - 1);
this.y = Math.min(this.y, newRows - 1);
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.savedY = Math.min(this.savedY, newRows - 1);
this.savedX = Math.min(this.savedX, newCols - 1);
this.scrollTop = 0;
}
@@ -222,7 +213,10 @@ export class Buffer implements IBuffer {
// needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol;
endCol = endCol || line.length;
// Only set endCol to the line length when it is null. 0 is a valid column.
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer';
import { MockTerminal } from './utils/TestUtils.test';
+15 -4
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, IBufferSet } from './Interfaces';
import { ITerminal, IBufferSet } from './Types';
import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter';
@@ -61,24 +61,35 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the normal Buffer of the BufferSet as its currently active Buffer
*/
public activateNormalBuffer(): void {
if (this._activeBuffer === this._normal) {
return;
}
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
this._alt.clear();
this._activeBuffer = this._normal;
this.emit('activate', this._normal);
this.emit('activate', {
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
}
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
if (this._activeBuffer === this._alt) {
return;
}
// Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it.
this._alt.fillViewportRows();
this._activeBuffer = this._alt;
this.emit('activate', this._alt);
this.emit('activate', {
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
}
/**
+22 -12
View File
@@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1])
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1])
if (ucs > data[mid][1]) {
min = mid + 1;
else if (ucs < data[mid][0])
} else if (ucs < data[mid][0]) {
max = mid - 1;
else
} else {
return true;
}
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0)
if (ucs === 0) {
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
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;
@@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH))
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
@@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
@@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
if (num < 32) {
return control | 0;
if (num < 127)
}
if (num < 127) {
return 1;
}
let t = table || init_table();
if (num < 65536)
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
+3 -3
View File
@@ -3,19 +3,19 @@
* @license MIT
*/
import { Charset } from './Types';
import { ICharset } from './Types';
/**
* The character sets supported by the terminal. These enable several languages
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported.
*/
export const CHARSETS: { [key: string]: Charset } = {};
export const CHARSETS: { [key: string]: ICharset } = {};
/**
* The default character set, US.
*/
export const DEFAULT_CHARSET: Charset = CHARSETS['B'];
export const DEFAULT_CHARSET: ICharset = CHARSETS['B'];
/**
* DEC Special Character and Line Drawing Set.
+3 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
interface IPosition {
start: number;
@@ -217,7 +217,7 @@ export class CompositionHelper {
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
};
}
/**
* Clears the textarea's position so that the cursor does not blink on IE.
@@ -226,5 +226,5 @@ export class CompositionHelper {
private clearTextareaPosition(): void {
this.textarea.style.left = '';
this.textarea.style.top = '';
};
}
}
+1 -1
View File
@@ -76,4 +76,4 @@ export namespace C0 {
export const SP = '\x20';
/** Delete (Caret = ^?) */
export const DEL = '\x7f';
};
}
-12
View File
@@ -13,18 +13,6 @@ describe('EventEmitter', () => {
eventEmitter = new EventEmitter();
});
describe('once', () => {
it('should trigger the listener only once', () => {
let count = 0;
const listener = () => count++;
eventEmitter.once('test', listener);
eventEmitter.emit('test');
assert.equal(count, 1);
eventEmitter.emit('test');
assert.equal(count, 1);
});
});
describe('emit', () => {
it('should emit events to listeners', () => {
let count1 = 0;
+6 -16
View File
@@ -3,10 +3,10 @@
* @license MIT
*/
import { IEventEmitter, IListenerType } from './Interfaces';
import { IEventEmitter } from 'xterm';
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: IListenerType[]};
private _events: {[type: string]: ((...args: any[]) => void)[]};
constructor() {
// Restore the previous events if available, this will happen if the
@@ -14,12 +14,12 @@ export class EventEmitter implements IEventEmitter {
this._events = this._events || {};
}
public on(type: string, listener: IListenerType): void {
public on(type: string, listener: ((...args: any[]) => void)): void {
this._events[type] = this._events[type] || [];
this._events[type].push(listener);
}
public off(type: string, listener: IListenerType): void {
public off(type: string, listener: ((...args: any[]) => void)): void {
if (!this._events[type]) {
return;
}
@@ -28,7 +28,7 @@ export class EventEmitter implements IEventEmitter {
let i = obj.length;
while (i--) {
if (obj[i] === listener || obj[i].listener === listener) {
if (obj[i] === listener) {
obj.splice(i, 1);
return;
}
@@ -41,16 +41,6 @@ export class EventEmitter implements IEventEmitter {
}
}
public once(type: string, listener: IListenerType): void {
function on(): void {
let args = Array.prototype.slice.call(arguments);
this.off(type, on);
listener.apply(this, args);
}
(<any>on).listener = listener;
this.on(type, on);
}
public emit(type: string, ...args: any[]): void {
if (!this._events[type]) {
return;
@@ -61,7 +51,7 @@ export class EventEmitter implements IEventEmitter {
}
}
public listeners(type: string): IListenerType[] {
public listeners(type: string): ((...args: any[]) => void)[] {
return this._events[type] || [];
}
+9 -11
View File
@@ -4,10 +4,9 @@
* @license MIT
*/
import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces';
import { CharData, IInputHandler, IInputHandlingTerminal, ITerminal } from './Types';
import { C0 } from './EscapeSequences';
import { DEFAULT_CHARSET } from './Charsets';
import { CharData } from './Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
@@ -26,7 +25,7 @@ export class InputHandler implements IInputHandler {
if (char >= ' ') {
// calculate print space
// expensive call, therefore we save width in line buffer
const ch_width = wcwidth(code);
const chWidth = wcwidth(code);
if (this._terminal.charset && this._terminal.charset[char]) {
char = this._terminal.charset[char];
@@ -36,7 +35,7 @@ export class InputHandler implements IInputHandler {
// insert combining char in last cell
// FIXME: needs handling after cursor jumps
if (!ch_width && this._terminal.buffer.x) {
if (!chWidth && this._terminal.buffer.x) {
// dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
@@ -56,7 +55,7 @@ export class InputHandler implements IInputHandler {
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
if (this._terminal.buffer.x + chWidth - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
this._terminal.buffer.x = 0;
@@ -70,8 +69,9 @@ export class InputHandler implements IInputHandler {
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
}
} else {
if (ch_width === 2) // FIXME: check for xterm behavior
if (chWidth === 2) { // FIXME: check for xterm behavior
return;
}
}
}
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
@@ -79,7 +79,7 @@ export class InputHandler implements IInputHandler {
// insert mode: move characters to right
if (this._terminal.insertMode) {
// do this twice for a fullwidth char
for (let moves = 0; moves < ch_width; ++moves) {
for (let moves = 0; moves < chWidth; ++moves) {
// remove last cell, if it's width is 0
// we have to adjust the second last cell as well
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
@@ -94,12 +94,12 @@ export class InputHandler implements IInputHandler {
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
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) {
if (chWidth === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
}
@@ -928,7 +928,6 @@ export class InputHandler implements IInputHandler {
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer();
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
@@ -1100,7 +1099,6 @@ export class InputHandler implements IInputHandler {
// if (params[0] === 1049) {
// this.restoreCursor(params);
// }
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
-354
View File
@@ -1,354 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
bracketedPasteMode: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
reset(): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
refresh(start: number, end: number): void;
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: Charset;
gcharset: number;
glevel: number;
charsets: Charset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
}
export interface ITerminalOptions {
bellSound?: string;
bellStyle?: string;
cancelEvents?: boolean;
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
enableBold?: boolean;
fontSize?: number;
fontFamily?: string;
handler?: (data: string) => void;
letterSpacing?: number;
lineHeight?: number;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
tabStopWidth?: number;
termName?: string;
theme?: ITheme;
useFlowControl?: boolean;
}
export interface IBuffer {
lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface IViewport {
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setBuffer(buffer: IBuffer): void;
setSelection(row: number, col: number, length: number): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface IEventEmitter {
on(type: string, listener: IListenerType): void;
off(type: string, listener: IListenerType): void;
emit(type: string, data?: any): void;
}
export interface IListenerType {
(data?: any): void;
listener?: (data?: any) => void;
};
export interface ILinkMatcherOptions {
/**
* 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;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* 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.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
cursor?: string;
cursorAccent?: string;
selection?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+3 -4
View File
@@ -4,10 +4,9 @@
*/
import { assert } from 'chai';
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types';
import { Linkifier } from './Linkifier';
import { LinkMatcher, LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
@@ -17,7 +16,7 @@ class TestLinkifier extends Linkifier {
Linkifier.TIME_BEFORE_LINKIFY = 0;
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
}
+20 -14
View File
@@ -3,9 +3,8 @@
* @license MIT
*/
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
import { IMouseZoneManager } from './input/Interfaces';
import { IMouseZoneManager } from './input/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
@@ -44,7 +43,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: LinkMatcher[] = [];
protected _linkMatchers: ILinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
@@ -82,12 +81,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
// Increase range to linkify
if (!this._rowsToLinkify.start) {
if (this._rowsToLinkify.start === null) {
this._rowsToLinkify.start = start;
this._rowsToLinkify.end = end;
} else {
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end;
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start);
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end);
}
// Clear out any existing links on this row range
@@ -143,7 +142,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
throw new Error('handler must be defined');
}
const matcher: LinkMatcher = {
const matcher: ILinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
@@ -151,6 +150,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
@@ -163,7 +163,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* considered after older link matchers.
* @param matcher The link matcher to be added.
*/
private _addLinkMatcherToList(matcher: LinkMatcher): void {
private _addLinkMatcherToList(matcher: ILinkMatcher): void {
if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher);
return;
@@ -219,7 +219,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
@@ -264,7 +264,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
*/
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
this._mouseZoneManager.add(new MouseZone(
x + 1,
x + 1 + uri.length,
@@ -276,21 +276,27 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.HOVER, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer';
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.TOOLTIP, <ILinkHoverEvent>{ x, y, length: uri.length});
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.LEAVE, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = '';
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
}
return true;
}
));
}
+10 -2
View File
@@ -5,7 +5,7 @@
*/
import { C0 } from './EscapeSequences';
import { IInputHandler } from './Interfaces';
import { IInputHandler } from './Types';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
@@ -224,8 +224,9 @@ export class Parser {
ch += data.charAt(this._position + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF)
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
switch (this._state) {
case ParserState.NORMAL:
@@ -286,36 +287,43 @@ export class Parser {
// Single Shift Select of G2 Character Set
// ( SS2 is 0x8e). This affects next character only.
case 'N':
this._state = ParserState.NORMAL;
break;
// ESC O
// Single Shift Select of G3 Character Set
// ( SS3 is 0x8f). This affects next character only.
case 'O':
this._state = ParserState.NORMAL;
break;
// ESC n
// Invoke the G2 Character Set as GL (LS2).
case 'n':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC o
// Invoke the G3 Character Set as GL (LS3).
case 'o':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC |
// Invoke the G3 Character Set as GR (LS3R).
case '|':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC }
// Invoke the G2 Character Set as GR (LS2R).
case '}':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC ~
// Invoke the G1 Character Set as GR (LS1R).
case '~':
this._terminal.setgLevel(1);
this._state = ParserState.NORMAL;
break;
// ESC 7 Save Cursor (DECSC).
+4 -6
View File
@@ -5,14 +5,13 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ICircularList, IBuffer } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
import { LineData, CharData } from './Types';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
@@ -21,16 +20,15 @@ class TestMockTerminal extends MockTerminal {
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: IBuffer,
charMeasure: CharMeasure
) {
super(terminal, buffer, charMeasure);
super(terminal, 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); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
// Disable DOM interaction
public enable(): void {}
@@ -59,7 +57,7 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, buffer, null);
selectionManager = new TestSelectionManager(terminal, null);
});
function stringToRow(text: string): LineData {
+84 -28
View File
@@ -3,15 +3,15 @@
* @license MIT
*/
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './utils/Browser';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import { LineData, CharData } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -29,6 +29,12 @@ const DRAG_SCROLL_MAX_SPEED = 15;
*/
const DRAG_SCROLL_INTERVAL = 50;
/**
* The maximum amount of time that can have elapsed for an alt click to move the
* cursor.
*/
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
/**
* A string containing all characters that are considered word separated by the
* double click to select work logic.
@@ -95,10 +101,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: (...args: any[]) => void;
private _mouseDownTimeStamp: number;
constructor(
private _terminal: ITerminal,
private _buffer: IBuffer,
private _charMeasure: CharMeasure
) {
super();
@@ -109,18 +117,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
/**
* Initializes listener variables.
*/
private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
// 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.lines.on('trim', (amount: number) => this._onTrim(amount));
this.initBuffersListeners();
}
public initBuffersListeners(): void {
this._terminal.buffer.lines.on('trim', this._trimListener);
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
}
/**
@@ -139,16 +153,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
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: IBuffer): void {
this._buffer = buffer;
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
@@ -249,6 +253,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
}
/**
* Checks if the current click was inside the current selection
* @param event The mouse event
*/
public isClickInSelection(event: MouseEvent): boolean {
const coords = this._getMouseBufferCoords(event);
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
}
/**
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
public selectWordAtCursor(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._model.selectionEnd = null;
this.refresh(true);
}
}
/**
* Selects all text within the terminal.
*/
@@ -274,7 +309,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
@@ -294,7 +329,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
@@ -322,6 +357,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mousedown event.
*/
public onMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// 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) {
@@ -440,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords);
this._selectWordAt(coords, true);
}
}
@@ -528,7 +564,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
} else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
@@ -541,10 +577,25 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent): void {
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
if (this.hasSelection)
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal)).move();
} else if (this.hasSelection) {
this._terminal.emit('selection');
}
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// 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.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
}
/**
@@ -582,7 +633,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* 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 {
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
@@ -688,15 +739,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
return null;
}
return { start, length };
}
/**
* Selects the word at the coordinates specified.
* @param coords The coordinates to get the word at.
* @param allowWhitespaceOnlySelection If whitespace should be selected
*/
protected _selectWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
if (wordPosition) {
this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length;
@@ -708,7 +764,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
const wordPosition = this._getWordAt(coords, true);
if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
}
+2 -2
View File
@@ -4,9 +4,9 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet';
import { BufferSet } from './BufferSet';
import { MockTerminal } from './utils/TestUtils.test';
class TestSelectionModel extends SelectionModel {
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
/**
* Represents a selection within the buffer. This model only cares about column
+18 -18
View File
@@ -15,17 +15,17 @@ import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
let primitive_pty: any;
let primitivePty: any;
// fake sychronous pty write - read
// 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 ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitive_pty.slave, data);
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
let b = new Buffer(64000);
let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null);
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
});
}
@@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void {
/* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states
function formatError(in_: string, out_: string, expected: string): string {
function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
let counter = start || 0;
return function(s: string): string {
@@ -47,9 +47,9 @@ function formatError(in_: string, out_: string, expected: string): string {
}
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(in_);
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n';
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s;
@@ -58,15 +58,15 @@ function formatError(in_: string, out_: string, expected: string): string {
// simple debug output of terminal cells
function terminalToString(term: Terminal): string {
let result = '';
let line_s = '';
let lineText = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
line_s = '';
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');
result += line_s;
lineText = lineText.replace(/\s+$/, '');
result += lineText;
result += '\n';
}
return result;
@@ -83,7 +83,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitive_pty = pty.native.open(COLS, ROWS);
primitivePty = pty.native.open(COLS, ROWS);
/** tests */
describe('xterm output comparison', () => {
@@ -118,24 +118,24 @@ if (os.platform() !== 'win32') {
((filename: string) => {
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
let in_file = fs.readFileSync(filename, 'utf8');
ptyWriteRead(in_file, from_pty => {
let inFile = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(from_pty);
xterm.writeBuffer.push(fromPty);
xterm.innerWrite();
let from_emulator = terminalToString(xterm);
let fromEmulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
let 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.
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (from_emulator !== expectedRightTrimmed) {
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(in_file, from_emulator, expected));
throw new Error(formatError(inFile, fromEmulator, expected));
// throw new Error('mismatch');
}
done();
+32
View File
@@ -503,6 +503,9 @@ describe('term.js addons', () => {
it('should return \\x1b[5C for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
@@ -515,6 +518,19 @@ describe('term.js addons', () => {
it('should return \\x1bf for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
});
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
@@ -597,6 +613,22 @@ describe('term.js addons', () => {
};
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should interfere with the alt key on keyDown', () => {
evKeyDown.altKey = true;
evKeyDown.keyCode = 81;
assert.equal(term.keyDown(evKeyDown), false);
evKeyDown.altKey = true;
evKeyDown.keyCode = 192;
assert.equal(term.keyDown(evKeyDown), false);
});
});
describe('On Mac OS', () => {
beforeEach(() => {
term.browser.isMac = true;
+92 -68
View File
@@ -21,6 +21,9 @@
* http://linux.die.net/man/7/urxvt
*/
import { ICharset, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { IMouseZoneManager } from './input/Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
import { Buffer, MAX_BUFFER_SIZE } from './Buffer';
import { CompositionHelper } from './CompositionHelper';
@@ -35,17 +38,13 @@ import { Renderer } from './renderer/Renderer';
import { Linkifier } from './Linkifier';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './utils/CharMeasure';
import * as Browser from './utils/Browser';
import * as Browser from './shared/utils/Browser';
import { MouseHelper } from './utils/MouseHelper';
import { CHARSETS } from './Charsets';
import { CustomKeyEventHandler, Charset, LinkMatcherHandler, LinkMatcherValidationCallback, CharData, LineData } from './Types';
import { ITerminal, IBrowser, ITerminalOptions, IInputHandlingTerminal, ILinkMatcherOptions, IViewport, ICompositionHelper, ITheme, ILinkifier } from './Interfaces';
import { BellSound } from './utils/Sounds';
import { BELL_SOUND } from './utils/Sounds';
import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager';
import { IMouseZoneManager } from './input/Interfaces';
import { MouseZoneManager } from './input/MouseZoneManager';
import { initialize as initializeCharAtlas } from './renderer/CharAtlas';
import { IRenderer } from './renderer/Interfaces';
import { ITheme } from 'xterm';
// Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null;
@@ -70,21 +69,26 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
termName: 'xterm',
cursorBlink: false,
cursorStyle: 'block',
bellSound: BellSound,
bellSound: BELL_SOUND,
bellStyle: 'none',
enableBold: true,
fontFamily: 'courier-new, courier, monospace',
fontSize: 15,
fontWeight: 'normal',
fontWeightBold: 'bold',
lineHeight: 1.0,
letterSpacing: 0,
scrollback: 1000,
screenKeys: false,
debug: false,
macOptionIsMeta: false,
cancelEvents: false,
disableStdin: false,
useFlowControl: false,
allowTransparency: false,
tabStopWidth: 8,
theme: null
theme: null,
rightClickSelectsWord: Browser.isMac
// programFeatures: false,
// focusKeys: false,
};
@@ -92,6 +96,7 @@ const DEFAULT_OPTIONS: ITerminalOptions = {
export class Terminal extends EventEmitter implements ITerminal, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement;
public element: HTMLElement;
public screenElement: HTMLElement;
/**
* The HTMLElement that the terminal is created in, set by Terminal.open.
@@ -131,10 +136,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// charset
// The current charset
public charset: Charset;
public charset: ICharset;
public gcharset: number;
public glevel: number;
public charsets: Charset[];
public charsets: ICharset[];
// mouse properties
private decLocator: boolean; // This is unstable and never set
@@ -181,7 +186,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
private writeStopped: boolean;
// leftover surrogate high from previous write invocation
private surrogate_high: string;
private surrogateHigh: string;
// Store if user went browsing history in scrollback
private userScrolling: boolean;
@@ -192,7 +197,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
public selectionManager: SelectionManager;
public linkifier: ILinkifier;
public buffers: BufferSet;
public buffer: Buffer;
public viewport: IViewport;
private compositionHelper: ICompositionHelper;
public charMeasure: CharMeasure;
@@ -280,7 +284,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.xoffSentToCatchUp = false;
this.writeStopped = false;
this.surrogate_high = '';
this.surrogateHigh = '';
this.userScrolling = false;
this.inputHandler = new InputHandler(this);
@@ -293,17 +297,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Create the terminal's buffers and set the current buffer
this.buffers = new BufferSet(this);
this.buffer = this.buffers.active; // Convenience shortcut;
this.buffers.on('activate', (buffer: Buffer) => {
this.buffer = buffer;
});
// Ensure the selection manager has the correct buffer
if (this.selectionManager) {
this.selectionManager.setBuffer(this.buffer);
this.selectionManager.clearSelection();
this.selectionManager.initBuffersListeners();
}
}
/**
* Convenience property to active buffer.
*/
public get buffer(): Buffer {
return this.buffers.active;
}
/**
* back_color_erase feature for xterm.
*/
@@ -361,6 +367,16 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
value = 'block';
}
break;
case 'fontWeight':
if (!value) {
value = 'normal';
}
break;
case 'fontWeightBold':
if (!value) {
value = 'bold';
}
break;
case 'lineHeight':
if (value < 1) {
console.warn(`${key} cannot be less than 1, value: ${value}`);
@@ -414,11 +430,14 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
case 'enableBold':
case 'letterSpacing':
case 'lineHeight':
case 'fontWeight':
case 'fontWeightBold':
const didCharSizeChange = (key === 'fontWeight' || key === 'fontWeightBold' || key === 'enableBold');
// When the font changes the size of the cells may change which requires a renderer clear
this.renderer.clear();
this.renderer.onResize(this.cols, this.rows, false);
this.renderer.onResize(this.cols, this.rows, didCharSizeChange);
this.refresh(0, this.rows - 1);
// this.charMeasure.measure(this.options);
case 'scrollback':
this.buffers.resize(this.cols, this.rows);
this.viewport.syncScrollArea();
@@ -443,7 +462,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.element.classList.add('focus');
this.showCursor();
this.emit('focus');
};
}
/**
* Blur the terminal, calling the blur function on the terminal's underlying
@@ -489,12 +508,12 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// Firefox doesn't appear to fire the contextmenu event on right click
on(this.element, 'mousedown', (event: MouseEvent) => {
if (event.button === 2) {
rightClickHandler(event, this.textarea, this.selectionManager);
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
}
});
} else {
on(this.element, 'contextmenu', (event: MouseEvent) => {
rightClickHandler(event, this.textarea, this.selectionManager);
rightClickHandler(event, this.textarea, this.selectionManager, this.options.rightClickSelectsWord);
});
}
@@ -574,8 +593,6 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.document = this.parent.ownerDocument;
this.body = <HTMLBodyElement>this.document.body;
initializeCharAtlas(this.document);
// Create main element container
this.element = this.document.createElement('div');
this.element.classList.add('terminal');
@@ -593,15 +610,18 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.viewportScrollArea.classList.add('xterm-scroll-area');
this.viewportElement.appendChild(this.viewportScrollArea);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
this.screenElement = document.createElement('div');
this.screenElement.classList.add('xterm-screen');
// Create the container that will hold helpers like the textarea for
// capturing DOM Events. Then produce the helpers.
this.helperContainer = document.createElement('div');
this.helperContainer.classList.add('xterm-helpers');
fragment.appendChild(this.helperContainer);
this.screenElement.appendChild(this.helperContainer);
fragment.appendChild(this.screenElement);
this._mouseZoneManager = new MouseZoneManager(this);
this.on('scroll', () => this._mouseZoneManager.clearAll());
this.linkifier.attachToDom(this._mouseZoneManager);
this.textarea = document.createElement('textarea');
this.textarea.classList.add('xterm-helper-textarea');
@@ -637,11 +657,10 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.on('resize', () => this.renderer.onResize(this.cols, this.rows, false));
this.on('blur', () => this.renderer.onBlur());
this.on('focus', () => this.renderer.onFocus());
window.addEventListener('resize', () => this.renderer.onWindowResize(window.devicePixelRatio));
this.charMeasure.on('charsizechanged', () => this.renderer.onResize(this.cols, this.rows, true));
this.renderer.on('resize', (dimensions) => this.viewport.syncScrollArea());
this.selectionManager = new SelectionManager(this, this.buffer, this.charMeasure);
this.selectionManager = new SelectionManager(this, this.charMeasure);
this.element.addEventListener('mousedown', (e: MouseEvent) => this.selectionManager.onMouseDown(e));
this.selectionManager.on('refresh', data => this.renderer.onSelectionChanged(data.start, data.end));
this.selectionManager.on('newselection', text => {
@@ -720,7 +739,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
button = getButton(ev);
// get mouse coordinates
pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
sendEvent(button, pos);
@@ -746,7 +765,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^[[M 3<^[[M@4<^[[M@5<^[[M@6<^[[M@7<^[[M#7<
function sendMove(ev: MouseEvent): void {
let button = pressed;
let pos = self.mouseHelper.getRawByteCoords(ev, self.element, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
let pos = self.mouseHelper.getRawByteCoords(ev, self.screenElement, self.charMeasure, self.options.lineHeight, self.cols, self.rows);
if (!pos) return;
// buttons marked as motions
@@ -1374,7 +1393,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true);
}
if (isThirdLevelShift(this.browser, ev)) {
if (this._isThirdLevelShift(this.browser, ev)) {
return true;
}
@@ -1395,6 +1414,19 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
return this.cancel(ev, true);
}
private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
/**
* Returns an object that determines how a KeyboardEvent should be handled. The key of the
* returned value is the new key code to pass to the PTY.
@@ -1449,6 +1481,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
if (ev.shiftKey) {
result.key = C0.BS; // ^H
break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^?
break;
}
result.key = C0.DEL; // ^?
break;
@@ -1551,21 +1586,23 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
break;
case 36:
// home
if (modifiers)
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
else if (this.applicationCursor)
} else if (this.applicationCursor) {
result.key = C0.ESC + 'OH';
else
} else {
result.key = C0.ESC + '[H';
}
break;
case 35:
// end
if (modifiers)
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
else if (this.applicationCursor)
} else if (this.applicationCursor) {
result.key = C0.ESC + 'OF';
else
} else {
result.key = C0.ESC + '[F';
}
break;
case 33:
// page up
@@ -1692,8 +1729,8 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if (!this.browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// On Mac this is a third level shift. Use <Esc> instead.
} else if ((!this.browser.isMac || this.options.macOptionIsMeta) && ev.altKey && !ev.ctrlKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = C0.ESC + String.fromCharCode(ev.keyCode + 32);
} else if (ev.keyCode === 192) {
@@ -1726,7 +1763,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
* @param g
* @param charset
*/
public setgCharset(g: number, charset: Charset): void {
public setgCharset(g: number, charset: ICharset): void {
this.charsets[g] = charset;
if (this.glevel === g) {
this.charset = charset;
@@ -1759,7 +1796,7 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
if (!key || (
(ev.altKey || ev.ctrlKey || ev.metaKey) && !isThirdLevelShift(this.browser, ev)
(ev.altKey || ev.ctrlKey || ev.metaKey) && !this._isThirdLevelShift(this.browser, ev)
)) {
return false;
}
@@ -2073,11 +2110,9 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
this.options.cols = this.cols;
const customKeyEventHandler = this.customKeyEventHandler;
const inputHandler = this.inputHandler;
const buffers = this.buffers;
this.setup();
this.customKeyEventHandler = customKeyEventHandler;
this.inputHandler = inputHandler;
this.buffers = buffers;
this.refresh(0, this.rows - 1);
this.viewport.syncScrollArea();
}
@@ -2106,13 +2141,15 @@ export class Terminal extends EventEmitter implements ITerminal, IInputHandlingT
}
private visualBell(): boolean {
return this.options.bellStyle === 'visual' ||
this.options.bellStyle === 'both';
return false;
// return this.options.bellStyle === 'visual' ||
// this.options.bellStyle === 'both';
}
private soundBell(): boolean {
return this.options.bellStyle === 'sound' ||
this.options.bellStyle === 'both';
return this.options.bellStyle === 'sound';
// return this.options.bellStyle === 'sound' ||
// this.options.bellStyle === 'both';
}
private syncBellSound(): void {
@@ -2153,19 +2190,6 @@ function off(el: any, type: string, handler: (event: Event) => any, capture: boo
el.removeEventListener(type, handler, capture);
}
function isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
const thirdLevelKey =
(browser.isMac && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
(browser.isMSWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
if (ev.type === 'keypress') {
return thirdLevelKey;
}
// Don't invoke for arrows, pageDown, home, backspace, etc. (on non-keypress events)
return thirdLevelKey && (!ev.keyCode || ev.keyCode > 47);
}
function wasMondifierKeyOnlyEvent(ev: KeyboardEvent): boolean {
return ev.keyCode === 16 || // Shift
ev.keyCode === 17 || // Ctrl
@@ -2218,7 +2242,7 @@ function matchColorDistance(r1: number, g1: number, b1: number, r2: number, g2:
return Math.pow(30 * (r1 - r2), 2)
+ Math.pow(59 * (g1 - g2), 2)
+ Math.pow(11 * (b1 - b2), 2);
};
}
function matchColor_(r1: number, g1: number, b1: number): number {
+333 -19
View File
@@ -3,33 +3,347 @@
* @license MIT
*/
export type LinkMatcher = {
id: number,
regex: RegExp,
handler: LinkMatcherHandler,
hoverTooltipCallback?: LinkMatcherHandler,
hoverLeaveCallback?: () => void,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type Charset = {[key: string]: string};
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkHoverEvent = {
x: number,
y: number,
length: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
};
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: ICharset;
gcharset: number;
glevel: number;
charsets: ICharset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
}
export interface IViewport {
scrollBarWidth: number;
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
/**
* Handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x: number;
y: number;
length: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
renderer: IRenderer;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
viewport: IViewport;
bracketedPasteMode: boolean;
applicationCursor: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
send(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
// TODO: The options that are not in the public API should be reviewed
export interface ITerminalOptions extends IPublicTerminalOptions {
cancelEvents?: boolean;
convertEol?: boolean;
debug?: boolean;
handler?: (data: string) => void;
screenKeys?: boolean;
termName?: string;
useFlowControl?: boolean;
}
export interface IBuffer {
lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet extends IEventEmitter {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setSelection(row: number, col: number, length: number): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
}
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ILinkMatcherOptions {
/**
* 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;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* 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;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
+20 -12
View File
@@ -3,15 +3,18 @@
* @license MIT
*/
import { ITerminal, IViewport } from './Interfaces';
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { IColorSet } from './renderer/Interfaces';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
public scrollBarWidth: number = 0;
private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0;
@@ -31,6 +34,10 @@ export class Viewport implements IViewport {
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
) {
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready.
@@ -48,13 +55,8 @@ export class Viewport implements IViewport {
private refresh(): void {
if (this.charMeasure.height > 0) {
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
}
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
@@ -93,6 +95,12 @@ export class Viewport implements IViewport {
* @param ev The scroll event.
*/
private onScroll(ev: Event): void {
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this.viewportElement.offsetParent) {
return;
}
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollLines(diff, true);
@@ -119,7 +127,7 @@ export class Viewport implements IViewport {
this.viewportElement.scrollTop += ev.deltaY * multiplier;
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
};
}
/**
* Handles the touchstart event, recording the touch occurred.
@@ -127,7 +135,7 @@ export class Viewport implements IViewport {
*/
public onTouchStart(ev: TouchEvent): void {
this.lastTouchY = ev.touches[0].pageY;
};
}
/**
* Handles the touchmove event, scrolling the viewport if the position shifted.
@@ -141,5 +149,5 @@ export class Viewport implements IViewport {
}
this.viewportElement.scrollTop += deltaY;
ev.preventDefault();
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as attach from './attach'
import * as attach from './attach';
class MockTerminal {}
describe('attach addon', () => {
describe('apply', () => {
it('should do register the `attach` and `detach` methods', () => {
attach.apply(MockTerminal);
attach.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function');
});
+55 -55
View File
@@ -5,118 +5,118 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function attach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <IAttachAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
var myTextDecoder;
let myTextDecoder;
term._getMessage = function(ev) {
var str;
if (typeof ev.data === "object") {
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
if (typeof ev.data === 'object') {
if (ev.data instanceof ArrayBuffer) {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
str = myTextDecoder.decode( ev.data );
}
else {
throw "TODO: handle Blob?";
} else {
throw 'TODO: handle Blob?';
}
}
if (buffered) {
term._pushToBuffer(str || ev.data);
addonTerminal.__pushToBuffer(str || ev.data);
} else {
term.write(str || ev.data);
addonTerminal.write(str || ev.data);
}
};
term._sendData = function(data) {
addonTerminal.__sendData = (data: string) => {
if (socket.readyState !== 1) {
return;
}
socket.send(data);
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', term.detach.bind(term, socket));
};
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Terminal} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function detach(term, socket) {
term.off('data', term._sendData);
export function detach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <IAttachAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
return attach(this, socket, bidirectional, buffered);
(<any>terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
attach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.detach = function(socket) {
return detach(this, socket);
(<any>terminalConstructor.prototype).detach = function (socket: WebSocket): void {
detach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fit from './fit'
import * as fit from './fit';
class MockTerminal {}
describe('fit addon', () => {
describe('apply', () => {
it('should do register the `proposeGeometry` and `fit` methods', () => {
fit.apply(MockTerminal);
fit.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function');
});
+41 -27
View File
@@ -13,43 +13,57 @@
* row and truncate its width with the current number of columns).
*/
export function proposeGeometry(term) {
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
export interface IGeometry {
rows: number;
cols: number;
}
export function proposeGeometry(term: Terminal): IGeometry {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
var elementStyle = window.getComputedStyle(term.element);
var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor;
var geometry = {
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(term.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
};
return geometry;
};
}
export function fit(term) {
var geometry = proposeGeometry(term);
export function fit(term: Terminal): void {
const geometry = proposeGeometry(term);
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term.renderer.clear();
(<any>term).renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.proposeGeometry = function() {
return proposeGeometry(this);
}
terminalConstructor.prototype.fit = function() {
return fit(this);
}
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {
return proposeGeometry(this);
};
(<any>terminalConstructor.prototype).fit = function (): void {
fit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'
import * as fullscreen from './fullscreen';
class MockTerminal {}
describe('fullscreen addon', () => {
describe('apply', () => {
it('should do register the `toggleFullscreen` method', () => {
fullscreen.apply(MockTerminal);
fullscreen.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
});
});
+13 -9
View File
@@ -3,15 +3,19 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
* Toggle the given terminal's fullscreen mode.
* @param {Terminal} term - The terminal to toggle full screen mode
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
* @param term The terminal to toggle full screen mode
* @param fullscreen Toggle fullscreen on (true) or off (false)
*/
export function toggleFullScreen(term, fullscreen) {
var fn;
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
let fn: string;
if (typeof fullscreen == 'undefined') {
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) {
fn = 'remove';
@@ -20,10 +24,10 @@ export function toggleFullScreen(term, fullscreen) {
}
term.element.classList[fn]('fullscreen');
};
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
return toggleFullScreen(this, fullscreen);
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {
toggleFullScreen(this, fullscreen);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
buffer: any;
selectionManager: any;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
}
+20 -6
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
// import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
interface ISearchResult {
term: string;
@@ -15,8 +14,8 @@ interface ISearchResult {
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper {
constructor(private _terminal: any) {
export class SearchHelper implements ISearchHelper {
constructor(private _terminal: ISearchAddonTerminal) {
// 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
@@ -114,8 +113,23 @@ export class SearchHelper {
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
const searchIndex = lowerStringLine.indexOf(lowerTerm);
let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
const line = this._terminal.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
searchIndex -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
if (charWidth === 0) {
searchIndex++;
}
}
return {
term,
col: searchIndex,
@@ -134,7 +148,7 @@ export class SearchHelper {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp, false);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
return true;
}
}
+25 -20
View File
@@ -3,8 +3,11 @@
* @license MIT
*/
import { SearchHelper } from './SearchHelper';
/// <reference path="../../../typings/xterm.d.ts"/>
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
@@ -12,12 +15,13 @@ import { SearchHelper } from './SearchHelper';
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findNext(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
export function findNext(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return (<SearchHelper>terminal.searchHelper).findNext(term);
};
return addonTerminal.__searchHelper.findNext(term);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
@@ -25,19 +29,20 @@ export function findNext(terminal: any, term: string): boolean {
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findPrevious(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
}
return (<SearchHelper>terminal.searchHelper).findPrevious(term);
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.findNext = function(term) {
return findNext(this, term);
}
terminalConstructor.prototype.findPrevious = function(term) {
return findPrevious(this, term);
export function findPrevious(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term);
};
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface ITerminadoAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
__setSize?(size: {rows: number, cols: number}): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as terminado from './terminado'
import * as terminado from './terminado';
class MockTerminal {}
describe('terminado addon', () => {
describe('apply', () => {
it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => {
terminado.apply(MockTerminal);
terminado.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function');
});
+52 -51
View File
@@ -6,106 +6,107 @@
* WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { ITerminadoAddonTerminal } from './Interfaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function terminadoAttach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
term._getMessage = function(ev) {
var data = JSON.parse(ev.data)
if( data[0] == "stdout" ) {
addonTerminal.__getMessage = (ev: MessageEvent) => {
const data = JSON.parse(ev.data);
if (data[0] === 'stdout') {
if (buffered) {
term._pushToBuffer(data[1]);
addonTerminal.__pushToBuffer(data[1]);
} else {
term.write(data[1]);
addonTerminal.write(data[1]);
}
}
};
term._sendData = function(data) {
addonTerminal.__sendData = (data: string) => {
socket.send(JSON.stringify(['stdin', data]));
};
term._setSize = function(size) {
addonTerminal.__setSize = (size: {rows: number, cols: number}) => {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
term.on('resize', term._setSize);
addonTerminal.on('resize', addonTerminal.__setSize);
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
};
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Xterm} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function terminadoDetach(term, socket) {
term.off('data', term._sendData);
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket - The socket to attach the current terminal.
* @param bidirectional - Whether the terminal should send data to the socket as well.
* @param buffered - Whether the rendering of incoming data should happen instantly or at a
* maximum frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) {
(<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
return terminadoAttach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.terminadoDetach = function(socket) {
(<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {
return terminadoDetach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
buffer: any;
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'
import * as winptyCompat from './winptyCompat';
class MockTerminal {}
describe('winptyCompat addon', () => {
describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(MockTerminal);
winptyCompat.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
});
});
+34 -27
View File
@@ -3,36 +3,43 @@
* @license MIT
*/
export function winptyCompatInit(terminal): void {
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
terminal.on('linefeed', () => {
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
const lastChar = line[terminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
});
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.winptyCompatInit = function(): void {
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
winptyCompatInit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as zmodem from './zmodem'
import * as zmodem from './zmodem';
class MockTerminal {}
describe('zmodem addon', () => {
describe('apply', () => {
it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => {
zmodem.apply(MockTerminal);
zmodem.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object');
});
+31 -29
View File
@@ -1,3 +1,12 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
*
* Allow xterm.js to handle ZMODEM uploads and downloads.
@@ -27,47 +36,40 @@
* via `detach()` and a re-`attach()`.)
*/
let Zmodem;
let zmodem;
export function zmodemAttach(term, ws, opts) {
if (!opts) opts = {};
export interface IZModemOptions {
noTerminalWriteOutsideSession?: boolean;
}
var senderFunc = function _ws_sender_func(octets) {
ws.send(new Uint8Array(octets));
};
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void {
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
var zsentry;
let zsentry;
function _shouldWrite() {
function _shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new Zmodem.Sentry( {
to_terminal: function _to_terminal(octets) {
zsentry = new zmodem.Sentry({
to_terminal: (octets: ArrayLike<number>) => {
if (_shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
}
},
sender: senderFunc,
on_retract: () => (<any>term).emit('zmodemRetract'),
on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)
});
on_retract: function _on_retract() {
term.emit('zmodemRetract');
},
function handleWSMessage(evt: MessageEvent): void {
on_detect: function _on_detect(detection) {
term.emit('zmodemDetect', detection);
},
} );
function handleWSMessage(evt) {
//In testing with xterm.js’s demo the first message was
//always text even if the rest were binary. While that
//may be specific to xterm.js’s demo, ultimately we
//should reject anything that isn’t binary.
// In testing with xterm.js’s demo the first message was
// always text even if the rest were binary. While that
// may be specific to xterm.js’s demo, ultimately we
// should reject anything that isn’t binary.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
term.write(evt.data);
@@ -82,9 +84,9 @@ export function zmodemAttach(term, ws, opts) {
ws.addEventListener('message', handleWSMessage);
}
export function apply(terminalConstructor) {
Zmodem = (typeof window == 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
export function apply(terminalConstructor: typeof Terminal): void {
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this);
terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this);
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
}
+261
View File
@@ -0,0 +1,261 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
enum Direction {
Up = 'A',
Down = 'B',
Right = 'C',
Left = 'D'
}
export class AltClickHandler {
private _startRow: number;
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<LineData>;
constructor(
private _mouseEvent: MouseEvent,
private _terminal: ITerminal
) {
this._lines = this._terminal.buffer.lines;
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols,
this._terminal.rows,
false
).map((coordinate: number) => {
return coordinate - 1;
});
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey) {
this._terminal.send(this._arrowSequences());
}
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
}
/**
* If the initial position of the cursor is on a row that is wrapped, move the
* cursor up to the first row that is not wrapped to have accurate vertical
* positioning.
*/
private _resetStartingRow(): string {
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let endRow = this._endRow;
if (this._moveToRequestedRow().length === 0) {
return '';
} else {
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.Left));
}
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
}
/**
* Move to the requested col on the ending row
*/
private _moveToRequestedCol(): string {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
let endRow = this._endRow;
let direction = this._horizontalDirection();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow,
direction === Direction.Right
).length, this._sequence(direction));
}
/**
* Utility functions
*/
/**
* Calculates the number of wrapped rows between the unwrapped starting and
* ending rows. These rows need to ignored since the cursor skips over them.
*/
private _wrappedRowsCount(): number {
let wrappedRows = 0;
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
let direction = this._verticalDirection() === Direction.Up ? -1 : 1;
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++;
}
}
return wrappedRows;
}
/**
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
private _horizontalDirection(): Direction {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
if ((this._startCol < this._endCol &&
startRow <= this._endRow) || // down/right or same y/right
(this._startCol >= this._endCol &&
startRow < this._endRow)) { // down/left or same y/left
return Direction.Right;
} else {
return Direction.Left;
}
}
/**
* Determines if the up or down arrow is needed
*/
private _verticalDirection(): Direction {
if (this._startRow > this._endRow) {
return Direction.Up;
} else {
return Direction.Down;
}
}
/**
* Constructs the string of chars in the buffer from a starting row and col
* to an ending row and col
* @param startCol The starting column position
* @param startRow The starting row position
* @param endCol The ending column position
* @param endRow The ending row position
* @param forward Direction to move
*/
private _bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean
): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > this._terminal.cols - 1) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = this._terminal.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
private _sequence(direction: Direction): string {
const mod = this._terminal.applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
}
/**
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}
+9 -3
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, ISelectionManager } from '../Interfaces';
import { ITerminal, ISelectionManager } from '../Types';
interface IWindow extends Window {
clipboardData?: {
@@ -100,6 +100,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.focus();
// Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled.
setTimeout(() => {
textarea.style.position = null;
textarea.style.width = null;
@@ -107,7 +108,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.style.left = null;
textarea.style.top = null;
textarea.style.zIndex = null;
}, 4);
}, 200);
}
/**
@@ -115,10 +116,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionManager The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea);
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
selectionManager.selectWordAtCursor(ev);
}
// Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText;
textarea.select();
+12 -11
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { IMouseZoneManager, IMouseZone } from './Interfaces';
import { ITerminal } from '../Interfaces';
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
const HOVER_DURATION = 500;
@@ -151,10 +151,10 @@ export class MouseZoneManager implements IMouseZoneManager {
// components from handling the mouse event.
const zone = this._findZoneEventAt(e);
if (zone) {
// TODO: When link modifier support is added, the event should only be
// cancelled when the modifier is held (see #1021)
e.preventDefault();
e.stopImmediatePropagation();
if (zone.willLinkActivate(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
}
@@ -169,7 +169,7 @@ export class MouseZoneManager implements IMouseZoneManager {
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
@@ -178,7 +178,7 @@ export class MouseZoneManager implements IMouseZoneManager {
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
return zone;
}
};
}
return null;
}
}
@@ -189,9 +189,10 @@ export class MouseZone implements IMouseZone {
public x2: number,
public y: number,
public clickCallback: (e: MouseEvent) => any,
public hoverCallback?: (e: MouseEvent) => any,
public tooltipCallback?: (e: MouseEvent) => any,
public leaveCallback?: () => void
public hoverCallback: (e: MouseEvent) => any,
public tooltipCallback: (e: MouseEvent) => any,
public leaveCallback: () => void,
public willLinkActivate: (e: MouseEvent) => boolean
) {
}
}
@@ -13,7 +13,8 @@ export interface IMouseZone {
x2: number;
y: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback?: (e: MouseEvent) => any;
tooltipCallback?: (e: MouseEvent) => any;
leaveCallback?: () => any;
hoverCallback: (e: MouseEvent) => any | undefined;
tooltipCallback: (e: MouseEvent) => any | undefined;
leaveCallback: () => any | undefined;
willLinkActivate: (e: MouseEvent) => boolean;
}
+45 -16
View File
@@ -3,10 +3,9 @@
* @license MIT
*/
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
import { ITerminal, ITerminalOptions } from '../Interfaces';
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal, ITerminalOptions } from '../Types';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CharData } from '../Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1;
@@ -25,22 +24,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _charAtlas: HTMLCanvasElement | ImageBitmap;
constructor(
container: HTMLElement,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this._initCanvas();
this._container.appendChild(this._canvas);
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer
if (!_alpha) {
if (!this._alpha) {
this.clearAll();
}
container.appendChild(this._canvas);
}
public onOptionsChanged(terminal: ITerminal): void {}
@@ -54,6 +56,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._refreshCharAtlas(terminal, colorSet);
}
protected setTransparency(terminal: ITerminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(terminal, this._colors);
this.onGridChanged(terminal, 0, terminal.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
@@ -64,7 +85,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
@@ -201,7 +222,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param color The color of the character.
*/
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
this._ctx.font = this._getFont(terminal, false);
this._ctx.textBaseline = 'top';
this._clipRow(terminal, y);
this._ctx.fillText(
@@ -269,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
charAtlasCellWidth,
this._scaledCharHeight);
} else {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim);
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
@@ -289,10 +310,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
this._ctx.save();
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (bold && terminal.options.enableBold) {
this._ctx.font = `bold ${this._ctx.font}`;
}
this._ctx.font = this._getFont(terminal, bold);
this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) {
@@ -332,5 +350,16 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._scaledCellHeight);
this._ctx.clip();
}
/**
* Gets the current font.
* @param terminal The terminal.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: ITerminal, isBold: boolean): string {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
}
}
+35 -121
View File
@@ -3,17 +3,21 @@
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { IColorSet } from '../renderer/Interfaces';
import { isFirefox } from '../utils/Browser';
import { ITerminal } from '../Types';
import { IColorSet } from './Types';
import { isFirefox } from '../shared/utils/Browser';
import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator';
export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
fontWeight: string;
fontWeightBold: string;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
@@ -63,8 +67,29 @@ export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledC
}
}
const canvasFactory = (width: number, height: number) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
const charAtlasConfig: ICharAtlasRequest = {
scaledCharWidth,
scaledCharHeight,
fontSize: terminal.options.fontSize,
fontFamily: terminal.options.fontFamily,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
background: colors.background,
foreground: colors.foreground,
ansiColors: colors.ansi,
devicePixelRatio: window.devicePixelRatio,
allowTransparency: terminal.options.allowTransparency
};
const newEntry: ICharAtlasCacheEntry = {
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig),
config: newConfig,
ownedBy: [terminal]
};
@@ -86,6 +111,9 @@ function generateConfig(scaledCharWidth: number, scaledCharHeight: number, termi
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
@@ -98,125 +126,11 @@ function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
let generator: CharAtlasGenerator;
/**
* Initializes the char atlas generator.
* @param document The document.
*/
export function initialize(document: Document): void {
if (!generator) {
generator = new CharAtlasGenerator(document);
}
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor(private _document: Document) {
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d', {alpha: false});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._canvas.width = 255 * cellWidth;
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
this._ctx.fillStyle = background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
this._ctx.save();
this._ctx.fillStyle = foreground;
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
this._ctx.restore();
}
// Default color bold
this._ctx.save();
this._ctx.font = `bold ${this._ctx.font}`;
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
this._ctx.restore();
}
this._ctx.restore();
// Colors 0-15
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = `bold ${this._ctx.font}`;
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillStyle = ansiColors[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
this._ctx.restore();
}
}
this._ctx.restore();
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox) {
// Regenerate canvas and context as they are now owned by the char atlas
const result = this._canvas;
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
return result;
}
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
// Remove the background color from the image so characters may overlap
const r = parseInt(background.substr(1, 2), 16);
const g = parseInt(background.substr(3, 2), 16);
const b = parseInt(background.substr(5, 2), 16);
this._clearColor(charAtlasImageData, r, g, b);
const promise = window.createImageBitmap(charAtlasImageData);
// Clear the rect while the promise is in progress
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
return promise;
}
private _clearColor(imageData: ImageData, r: number, g: number, b: number): void {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
}
+3 -19
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { IColorSet, IColorManager } from './Interfaces';
import { ITheme } from '../Interfaces';
import { IColorSet, IColorManager } from './Types';
import { ITheme } from 'xterm';
const DEFAULT_FOREGROUND = '#ffffff';
const DEFAULT_BACKGROUND = '#000000';
@@ -86,7 +86,7 @@ export class ColorManager implements IColorManager {
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
this.colors.background = theme.background || DEFAULT_BACKGROUND;
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
@@ -107,20 +107,4 @@ export class ColorManager implements IColorManager {
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
}
private _validateColor(color: string, fallback: string): string {
if (!color) {
return fallback;
}
if (color.length === 7 && color.charAt(0) === '#') {
return color;
}
if (color.length === 4 && color.charAt(0) === '#') {
const r = color.charAt(1);
const g = color.charAt(2);
const b = color.charAt(3);
return `#${r}${r}${g}${g}${b}${b}`;
}
return fallback;
}
}
+6 -7
View File
@@ -3,15 +3,13 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData } from '../Types';
import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types';
interface CursorState {
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
@@ -25,7 +23,7 @@ interface CursorState {
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: CursorState;
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean;
@@ -108,9 +106,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Only render if the animation frame is not active
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
}
-99
View File
@@ -1,99 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
+7 -9
View File
@@ -3,21 +3,19 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: LinkHoverEvent = null;
private _state: ILinkHoverEvent = null;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
super(container, 'link', zIndex, true, colors);
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
@@ -37,13 +35,13 @@ export class LinkRenderLayer extends BaseRenderLayer {
}
}
private _onLinkHover(e: LinkHoverEvent): void {
private _onLinkHover(e: ILinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length);
this._state = e;
}
private _onLinkLeave(e: LinkHoverEvent): void {
private _onLinkLeave(e: ILinkHoverEvent): void {
this._clearCurrentLink();
}
}
+62 -14
View File
@@ -3,16 +3,18 @@
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer {
/** A queue of the rows to be refreshed */
@@ -21,6 +23,9 @@ export class Renderer extends EventEmitter implements IRenderer {
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
public colorManager: ColorManager;
public dimensions: IRenderDimensions;
@@ -31,11 +36,12 @@ export class Renderer extends EventEmitter implements IRenderer {
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
];
this.dimensions = {
scaledCharWidth: null,
@@ -53,6 +59,24 @@ export class Renderer extends EventEmitter implements IRenderer {
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this.onOptionsChanged();
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
observer.observe(this._terminal.element);
}
}
public onIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.intersectionRatio === 0;
if (!this._isPaused && this._needsFullRefresh) {
this._terminal.refresh(0, this._terminal.rows - 1);
}
}
public onWindowResize(devicePixelRatio: number): void {
@@ -73,7 +97,11 @@ export class Renderer extends EventEmitter implements IRenderer {
l.reset(this._terminal);
});
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
return this.colorManager.colors;
}
@@ -86,7 +114,15 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
// Force a refresh
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', {
width: this.dimensions.canvasWidth,
@@ -99,27 +135,35 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onBlur(): void {
this._renderLayers.forEach(l => l.onBlur(this._terminal));
this._runOperation(l => l.onBlur(this._terminal));
}
public onFocus(): void {
this._renderLayers.forEach(l => l.onFocus(this._terminal));
this._runOperation(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end));
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
}
public onCursorMove(): void {
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
this._runOperation(l => l.onCursorMove(this._terminal));
}
public onOptionsChanged(): void {
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
this._runOperation(l => l.onOptionsChanged(this._terminal));
}
public clear(): void {
this._renderLayers.forEach(l => l.reset(this._terminal));
this._runOperation(l => l.reset(this._terminal));
}
private _runOperation(operation: (layer: IRenderLayer) => void): void {
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._renderLayers.forEach(l => operation(l));
}
}
/**
@@ -129,6 +173,10 @@ export class Renderer extends EventEmitter implements IRenderer {
* @param {number} end The end row.
*/
public queueRefresh(start: number, end: number): void {
if (this._isPaused) {
this._needsFullRefresh = true;
return;
}
this._refreshRowsQueue.push({ start: start, end: end });
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
+2 -3
View File
@@ -3,11 +3,10 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Types';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer {
+13 -11
View File
@@ -3,12 +3,10 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS } from './Types';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types';
import { GridCache } from './GridCache';
import { CharData } from '../Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
/**
@@ -24,8 +22,8 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'text', zIndex, false, colors);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
this._state = new GridCache<CharData>();
}
@@ -33,7 +31,7 @@ export class TextRenderLayer extends BaseRenderLayer {
super.resize(terminal, dim, charSizeChanged);
// Clear the character width cache if the font or width has changed
const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
const terminalFont = this._getFont(terminal, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
@@ -166,7 +164,7 @@ export class TextRenderLayer extends BaseRenderLayer {
this._ctx.save();
if (flags & FLAGS.BOLD) {
this._ctx.font = `bold ${this._ctx.font}`;
this._ctx.font = this._getFont(terminal, true);
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
@@ -192,9 +190,13 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
/**
* Whether a character is overlapping to the next cell.
*/
public onOptionsChanged(terminal: ITerminal): void {
this.setTransparency(terminal, terminal.options.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(charData: CharData): boolean {
// Only single cell characters can be overlapping, rendering issues can
// occur without this check
+98 -2
View File
@@ -3,7 +3,10 @@
* @license MIT
*/
/**
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
/**
* Flags used to render terminal text properly.
*/
export enum FLAGS {
@@ -13,4 +16,97 @@ export enum FLAGS {
INVERSE = 8,
INVISIBLE = 16,
DIM = 32
};
}
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
+141
View File
@@ -0,0 +1,141 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { FontWeight } from 'xterm';
import { isFirefox } from './utils/Browser';
declare const Promise: any;
export interface IOffscreenCanvas {
width: number;
height: number;
getContext(type: '2d', config?: Canvas2DContextAttributes): CanvasRenderingContext2D;
transferToImageBitmap(): ImageBitmap;
}
export interface ICharAtlasRequest {
scaledCharWidth: number;
scaledCharHeight: number;
fontSize: number;
fontFamily: string;
fontWeight: FontWeight;
fontWeightBold: FontWeight;
background: string;
foreground: string;
ansiColors: string[];
devicePixelRatio: number;
allowTransparency: boolean;
}
export const CHAR_ATLAS_CELL_SPACING = 1;
/**
* Generates a char atlas.
* @param context The window or worker context.
* @param canvasFactory A function to generate a canvas with a width or height.
* @param request The config for the new char atlas.
*/
export function generateCharAtlas(context: Window, canvasFactory: (width: number, height: number) => HTMLCanvasElement | IOffscreenCanvas, request: ICharAtlasRequest): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = request.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = request.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
const canvas = canvasFactory(
/*255 ascii chars*/255 * cellWidth,
(/*default+default bold*/2 + /*0-15*/16) * cellHeight
);
const ctx = canvas.getContext('2d', {alpha: request.allowTransparency});
ctx.fillStyle = request.background;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.fillStyle = request.foreground;
ctx.font = getFont(request.fontWeight, request);
ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
ctx.restore();
}
// Default color bold
ctx.save();
ctx.font = getFont(request.fontWeightBold, request);
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
ctx.clip();
ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
ctx.restore();
}
ctx.restore();
// Colors 0-15
ctx.font = getFont(request.fontWeight, request);
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
ctx.font = getFont(request.fontWeightBold, request);
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
ctx.save();
ctx.beginPath();
ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
ctx.clip();
ctx.fillStyle = request.ansiColors[colorIndex];
ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
ctx.restore();
}
}
ctx.restore();
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in context) || isFirefox) {
// Don't attempt to clear background colors if createImageBitmap is not supported
if (canvas instanceof HTMLCanvasElement) {
// Just return the HTMLCanvas if it's a HTMLCanvasElement
return canvas;
} else {
// Transfer to an ImageBitmap is this is an OffscreenCanvas
return new Promise(r => r(canvas.transferToImageBitmap()));
}
}
const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Remove the background color from the image so characters may overlap
const r = parseInt(request.background.substr(1, 2), 16);
const g = parseInt(request.background.substr(3, 2), 16);
const b = parseInt(request.background.substr(5, 2), 16);
clearColor(charAtlasImageData, r, g, b);
return context.createImageBitmap(charAtlasImageData);
}
/**
* Makes a partiicular rgb color in an ImageData completely transparent.
*/
function clearColor(imageData: ImageData, r: number, g: number, b: number): void {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
function getFont(fontWeight: FontWeight, request: ICharAtlasRequest): string {
return `${fontWeight} ${request.fontSize * request.devicePixelRatio}px ${request.fontFamily}`;
}
@@ -3,8 +3,6 @@
* @license MIT
*/
import { contains } from './Generic';
const isNode = (typeof navigator === 'undefined') ? true : false;
const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
@@ -20,3 +18,12 @@ 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;
/**
* Return if the given array contains the given element
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
}
+1 -1
View File
@@ -4,8 +4,8 @@
*/
import jsdom = require('jsdom');
import { ICharMeasure, ITerminal } from '../Types';
import { assert } from 'chai';
import { ICharMeasure, ITerminal } from '../Interfaces';
import { CharMeasure } from './CharMeasure';
describe('CharMeasure', () => {
+1 -1
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Types';
import { EventEmitter } from '../EventEmitter';
import { ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
/**
* Utility class that measures the size of a character. Measurements are done in
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { EventEmitter } from '../EventEmitter';
import { ICircularList } from '../Interfaces';
import { ICircularList } from '../Types';
/**
* Represents a circular list; a list with a maximum size that wraps around when push is called,
-13
View File
@@ -1,13 +0,0 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* Return if the given array contains the given element
* @param {Array} array The array to search for the given element.
* @param {Object} el The element to look for into the array
*/
export function contains(arr: any[], el: any): boolean {
return arr.indexOf(el) >= 0;
};
+3 -3
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { ICharMeasure } from '../Interfaces';
import { IRenderer } from '../renderer/Interfaces';
import { ICharMeasure } from '../Types';
import { IRenderer } from '../renderer/Types';
export class MouseHelper {
constructor(private _renderer: IRenderer) {}
@@ -24,7 +24,7 @@ export class MouseHelper {
while (element) {
x -= element.offsetLeft;
y -= element.offsetTop;
element = 'offsetParent' in element ? <HTMLElement>element.offsetParent : <HTMLElement>element.parentElement;
element = <HTMLElement>element.offsetParent;
}
element = originalElement;
while (element && element !== element.ownerDocument.body) {
+55
View File
@@ -0,0 +1,55 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
/**
* The screen device pixel ratio monitor allows listening for when the
* window.devicePixelRatio value changes. This is done not with polling but with
* the use of window.matchMedia to watch media queries. When the event fires,
* the listener will be reattached using a different media query to ensure that
* any further changes will register.
*
* The listener should fire on both window zoom changes and switching to a
* monitor with a different DPI.
*/
export class ScreenDprMonitor {
private _currentDevicePixelRatio: number;
private _outerListener: MediaQueryListListener;
private _listener: ScreenDprListener;
private _resolutionMediaMatchList: MediaQueryList;
public setListener(listener: ScreenDprListener): void {
if (this._listener) {
this.clearListener();
}
this._listener = listener;
this._outerListener = () => {
this._listener(window.devicePixelRatio, this._currentDevicePixelRatio);
this._updateDpr();
};
this._updateDpr();
}
private _updateDpr(): void {
// Clear listeners for old DPR
if (this._resolutionMediaMatchList) {
this._resolutionMediaMatchList.removeListener(this._outerListener);
}
// Add listeners for new DPR
this._currentDevicePixelRatio = window.devicePixelRatio;
this._resolutionMediaMatchList = window.matchMedia(`screen and (resolution: ${window.devicePixelRatio}dppx)`);
this._resolutionMediaMatchList.addListener(this._outerListener);
}
public clearListener(): void {
if (!this._listener) {
return;
}
this._resolutionMediaMatchList.removeListener(this._outerListener);
this._listener = null;
this._outerListener = null;
}
}
+1 -1
View File
@@ -7,4 +7,4 @@
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const BellSound = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export const BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
+76 -9
View File
@@ -3,13 +3,76 @@
* @license MIT
*/
import { ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, IListenerType, IInputHandlingTerminal, IViewport, ICircularList, ICompositionHelper, ITheme, ILinkifier, IMouseHelper } from '../Interfaces';
import { LineData } from '../Types';
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions } from '../Types';
import { Buffer } from '../Buffer';
import * as Browser from './Browser';
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Interfaces';
import * as Browser from '../shared/utils/Browser';
import { ITheme } from 'xterm';
export class MockTerminal implements ITerminal {
getOption(key: any): any {
throw new Error('Method not implemented.');
}
setOption(key: any, value: any): void {
throw new Error('Method not implemented.');
}
blur(): void {
throw new Error('Method not implemented.');
}
focus(): void {
throw new Error('Method not implemented.');
}
resize(columns: number, rows: number): void {
throw new Error('Method not implemented.');
}
writeln(data: string): void {
throw new Error('Method not implemented.');
}
open(parent: HTMLElement): void {
throw new Error('Method not implemented.');
}
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
throw new Error('Method not implemented.');
}
deregisterLinkMatcher(matcherId: number): void {
throw new Error('Method not implemented.');
}
hasSelection(): boolean {
throw new Error('Method not implemented.');
}
getSelection(): string {
throw new Error('Method not implemented.');
}
clearSelection(): void {
throw new Error('Method not implemented.');
}
selectAll(): void {
throw new Error('Method not implemented.');
}
destroy(): void {
throw new Error('Method not implemented.');
}
scrollPages(pageCount: number): void {
throw new Error('Method not implemented.');
}
scrollToTop(): void {
throw new Error('Method not implemented.');
}
scrollToBottom(): void {
throw new Error('Method not implemented.');
}
clear(): void {
throw new Error('Method not implemented.');
}
write(data: string): void {
throw new Error('Method not implemented.');
}
send(data: string): void {
throw new Error('Method not implemented.');
}
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
@@ -17,6 +80,7 @@ export class MockTerminal implements ITerminal {
isFocused: boolean;
options: ITerminalOptions = {};
element: HTMLElement;
screenElement: HTMLElement;
rowContainer: HTMLElement;
selectionContainer: HTMLElement;
selectionManager: ISelectionManager;
@@ -33,13 +97,15 @@ export class MockTerminal implements ITerminal {
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
applicationCursor: boolean;
handler(data: string): void {
throw new Error('Method not implemented.');
}
on(event: string, callback: () => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: IListenerType): void {
off(type: string, listener: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
scrollLines(disp: number, suppressScrollEvent: boolean): void {
@@ -184,10 +250,10 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
setOption(key: string, value: any): void {
this.options[key] = value;
}
on(type: string, listener: IListenerType): void {
on(type: string, listener: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: IListenerType): void {
off(type: string, listener: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
@@ -220,10 +286,10 @@ export class MockBuffer implements IBuffer {
export class MockRenderer implements IRenderer {
colorManager: IColorManager;
on(type: string, listener: IListenerType): void {
on(type: string, listener: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
off(type: string, listener: IListenerType): void {
off(type: string, listener: (...args: any[]) => void): void {
throw new Error('Method not implemented.');
}
emit(type: string, data?: any): void {
@@ -244,6 +310,7 @@ export class MockRenderer implements IRenderer {
}
export class MockViewport implements IViewport {
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
}
+11 -1
View File
@@ -95,9 +95,19 @@
/* On OS X this is required in order for the scroll bar to appear fully opaque */
background-color: #000;
overflow-y: scroll;
cursor: default;
position: absolute;
right: 0;
left: 0;
top: 0;
bottom: 0;
}
.xterm canvas {
.xterm .xterm-screen {
position: relative;
}
.xterm .xterm-screen canvas {
position: absolute;
left: 0;
top: 0;
+23 -6
View File
@@ -1,27 +1,38 @@
{
"rules": {
"array-type": [
true,
"array"
],
"class-name": true,
"comment-format": [
true,
"check-space"
],
"curly": [
true,
"ignore-same-line"
],
"indent": [
true,
"spaces"
],
"interface-name": [
true,
"always-prefix"
],
"interface-over-type-literal": true,
"typedef": [
true,
"call-signature",
"parameter"
],
"eofline": true,
"no-duplicate-imports": true,
"no-eval": true,
"no-internal-module": true,
"no-trailing-whitespace": true,
"one-variable-per-declaration": [
true,
true
],
"one-variable-per-declaration": true,
"no-unsafe-finally": true,
"no-var-keyword": true,
"quotemark": [
@@ -48,15 +59,21 @@
],
"variable-name": [
true,
"ban-keywords"
"ban-keywords",
"check-format",
"allow-leading-underscore"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-module",
"check-operator",
"check-rest-spread",
"check-separator",
"check-type"
"check-type",
"check-type-operator",
"check-preblock"
]
}
}
+63 -23
View File
@@ -8,10 +8,21 @@
*/
declare module 'xterm' {
/**
* A string representing text font weight.
*/
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
/**
* An object containing start up options for the terminal.
*/
export interface ITerminalOptions {
/**
* Whether background should support non-opaque color. It must be set before
* executing open() method and can't be changed later without excuting it again.
* Warning: Enabling this option can reduce performances somewhat.
*/
allowTransparency?: boolean;
/**
* A data uri of the sound to use for the bell (needs bellStyle = 'sound').
*/
@@ -44,6 +55,8 @@ declare module 'xterm' {
/**
* Whether to enable the rendering of bold text.
*
* @deprecated Use fontWeight and fontWeightBold instead.
*/
enableBold?: boolean;
@@ -57,6 +70,16 @@ declare module 'xterm' {
*/
fontFamily?: string;
/**
* The font weight used to render non-bold text.
*/
fontWeight?: FontWeight;
/**
* The font weight used to render bold text.
*/
fontWeightBold?: FontWeight;
/**
* The spacing in whole pixels between characters..
*/
@@ -67,6 +90,17 @@ declare module 'xterm' {
*/
lineHeight?: number;
/**
* Whether to treat option as the meta key.
*/
macOptionIsMeta?: boolean;
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
*/
rightClickSelectsWord?: boolean;
/**
* The number of rows in the terminal.
*/
@@ -148,8 +182,8 @@ declare module 'xterm' {
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
* A callback that validates whether to create an individual link, pass
* whether the link is valid to the callback.
*/
validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void;
@@ -170,12 +204,26 @@ declare module 'xterm' {
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IEventEmitter {
on(type: string, listener: (...args: any[]) => void): void;
off(type: string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
}
/**
* The class that represents an xterm.js terminal.
*/
export class Terminal {
export class Terminal implements IEventEmitter {
/**
* The element containing the terminal.
*/
@@ -224,7 +272,7 @@ declare module 'xterm' {
* @param type The type of the event.
* @param listener The listener.
*/
on(type: 'data', listener: (data?: string) => void): void;
on(type: 'data', listener: (...args: any[]) => void): void;
/**
* Registers an event listener.
* @param type The type of the event.
@@ -275,6 +323,8 @@ declare module 'xterm' {
*/
off(type: 'blur' | 'focus' | 'linefeed' | 'selection' | 'data' | 'key' | 'keypress' | 'keydown' | 'refresh' | 'resize' | 'scroll' | 'title' | string, listener: (...args: any[]) => void): void;
emit(type: string, data?: any): void;
/**
* Resizes the terminal.
* @param x The number of columns to resize to.
@@ -346,22 +396,6 @@ declare module 'xterm' {
*/
selectAll(): void;
// /**
// * 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.
// */
// findNext(term: string): boolean;
// /**
// * 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.
// */
// findPrevious(term: string): boolean;
/**
* Destroys the terminal and detaches it from the DOM.
*/
@@ -404,12 +438,12 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'termName'): string;
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -437,6 +471,12 @@ declare module 'xterm' {
* @param value The option value.
*/
setOption(key: 'fontFamily' | 'termName' | 'bellSound', value: string): void;
/**
* Sets an option on the terminal.
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
/**
* Sets an option on the terminal.
* @param key The option key.
@@ -454,7 +494,7 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
setOption(key: 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.