Compare commits

...
273 Commits
Author SHA1 Message Date
Paris Kasidiaris d2b3af5aa8 Bump version to 3.5.1
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-07-13 06:45:40 +00:00
Daniel Imms 5f7eb6e51d Fix charset drawing
Fixes #1565
2018-07-12 08:46:00 -07:00
Paris Kasidiaris 7c3bf3a130 Bump version to 3.5.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-07-10 16:01:25 +00:00
Daniel Imms 5620da49d8 Merge pull request #1553 from Tyriar/1552_selection_state
Include ydisp in selection state
2018-07-06 10:18:06 -07:00
Daniel Imms 3c8f336689 Include ydisp in selection state
Fixes #1552
2018-07-06 09:53:45 -07:00
Daniel Imms f1c9d37c83 Merge pull request #1547 from Tyriar/remove_parser
Remove the old parser
2018-07-05 07:40:15 -07:00
Daniel Imms 08603c7975 Merge branch 'master' into remove_parser 2018-07-05 07:40:06 -07:00
Daniel Imms 318dc6fcf6 Merge pull request #1537 from Tyriar/fix_typo
Fix typo
2018-07-05 07:39:48 -07:00
Daniel Imms 68adadf3f4 Remove the old parser 2018-07-04 10:34:53 -07:00
Daniel Imms 73bc1c577a Merge pull request #1503 from Tyriar/998_mouse_off
Nullify and remove mouse handlers when mouse move it exited
2018-07-02 13:25:36 -07:00
Daniel Imms aec4c908d3 Merge remote-tracking branch 'origin/master' into 998_mouse_off 2018-07-02 13:22:28 -07:00
Daniel Imms 9aad2f7ca5 Merge pull request #1543 from nikonso/fix-SelectionRenderLayer-state
Fix SelectionRenderLayer._state
2018-07-02 09:23:01 -07:00
Daniel Imms af9d17760d Make columnSelectMode mandatory 2018-07-02 09:22:38 -07:00
Daniel Imms 4557f857f7 Merge pull request #1542 from nikonso/recreate-terminal
Add recreate terminal after disposing to the demo
2018-07-02 09:13:16 -07:00
Bruno Ribeito ed58c42513 Fix actions and styles 2018-07-01 22:50:08 +01:00
Bruno Ribeito 7e393dfa8c Move logic to helper function 2018-07-01 22:24:51 +01:00
Bruno Ribeiro 0a70b182a7 Merge branch 'master' into recreate-terminal 2018-07-01 20:35:15 +01:00
Bruno Ribeito 932a8d5028 Fix SelectionRenderLayer._state 2018-07-01 19:26:50 +01:00
Bruno Ribeito 054592520e Add recreate terminal after disposing to the demo 2018-07-01 18:56:58 +01:00
Daniel Imms 38debf77f8 Merge pull request #1538 from vsinha/column-select
Support for column/box selection
2018-06-30 10:20:38 -07:00
Daniel Imms 117c79e4e7 Polish 2018-06-30 10:11:48 -07:00
Viraj Sinha 3a4ad37177 Added mac platform check before we try to use mac specific config var 2018-06-29 20:19:18 -07:00
Viraj Sinha ace612e575 Fixes for merging column select 2018-06-29 20:15:17 -07:00
Daniel Imms e628a9be76 Fix emoji test 2018-06-29 11:25:17 -07:00
Daniel Imms 835d5a16ea Fix build after merge 2018-06-29 11:17:45 -07:00
Viraj Sinha 169cbb3fca Merge branch 'master' into column-select 2018-06-28 15:59:27 -07:00
Viraj Sinha 2669576b6c Adds a toggle to disable column selections on mac 2018-06-28 15:56:25 -07:00
Viraj Sinha 3202ddc96d Adds support for column selections if holding alt on mac and shift on windows/linux 2018-06-28 15:55:44 -07:00
Daniel Imms 35ee3610f7 Add missing new line back 2018-06-28 13:25:54 -07:00
Daniel Imms 45f5a710d6 Merge branch 'master' into fix_typo 2018-06-28 13:25:12 -07:00
Daniel Imms 1395b0f03c Merge pull request #1525 from Tyriar/1518_memory
Ensure all references are released when Terminal.dispose is called
2018-06-28 13:21:27 -07:00
Daniel Imms 38efb1a02d Merge branch 'master' into fix_typo 2018-06-28 13:00:34 -07:00
Daniel Imms 22308fb37c Fix typo 2018-06-28 12:59:53 -07:00
Daniel Imms 100143bd02 Merge branch 'master' into 1518_memory 2018-06-28 10:29:07 -07:00
Daniel Imms 8c20a2e26b Merge pull request #1535 from 7PH/master
#1532: 'Reset to Initial State' (RIS) escape sequence no longer makes the cursor disappear
2018-06-27 07:09:49 -07:00
Daniel Imms e24dca3e29 Send mousemove events when x10Mouse is true 2018-06-26 07:26:14 -07:00
7PH 218a4014c7 #1532: Add test to ensure terminal reset does not display cursor if hidden before 2018-06-26 10:42:08 +02:00
7PH 0ad7b8e27b #1532: Add test to ensure terminal reset does not affect cursorState 2018-06-26 10:36:11 +02:00
7PH f1504e5c0f #1532: Terminal reset does not affect cursorState anymore 2018-06-26 10:33:46 +02:00
Daniel Imms f0d872f7fe Merge pull request #1531 from Tyriar/pin_notepty
Pin node-pty to 0.7.6
2018-06-25 11:38:34 -07:00
Daniel Imms ff18b193eb Pin node-pty to 0.7.6 2018-06-25 11:28:09 -07:00
Daniel Imms 8ae8705ae6 Fix typo 2018-06-25 11:21:00 -07:00
Daniel Imms d585515de1 Move RenderDebouncer and ScreenDprMonitor into ui 2018-06-23 17:34:01 +10:00
Daniel Imms 8dda3e68f8 MOve CharMeasure into ui/ 2018-06-23 17:32:32 +10:00
Daniel Imms 4c7e5d2201 Move CircularList to common (tests in ui/ need) 2018-06-23 17:27:34 +10:00
Daniel Imms cc41fefe31 Move MouseZoneManager into ui/ 2018-06-23 17:25:07 +10:00
Daniel Imms 9a3751d7fb Merge pull request #1522 from 7PH/master
#1521: VT100 escape codes DECSC and DECRC now handle character attributes
2018-06-22 07:36:42 +10:00
PowerHat 64b0a9482c Merge branch 'master' into master 2018-06-21 11:35:01 +02:00
Daniel Imms 221a4353a4 Merge pull request #1524 from Tyriar/tyriar/1523
Make sure DOM renderer doesn't render cells beyond cols
2018-06-21 19:22:39 +10:00
Daniel Imms 989c24cbc3 Merge pull request #1513 from Tyriar/ensure_setting_redraws
Ensure drawBoldTextInBrightColors redraws screen
2018-06-21 19:06:00 +10:00
7PH 654f4bd5ae Merge branch 'master' of https://github.com/7PH/xterm.js 2018-06-21 09:22:41 +02:00
7PH 43ffa957ab #1521: fixed default value of 'Terminal.curAttr' when restoring a not previously saved cursor state 2018-06-21 09:22:17 +02:00
Daniel Imms a9a506f7db Merge branch 'master' into 998_mouse_off 2018-06-21 14:35:09 +10:00
Daniel Imms 6e71fb0fb8 Merge branch 'master' into ensure_setting_redraws 2018-06-21 14:33:42 +10:00
Daniel Imms 4d52cd3c66 Clean reference to custom key event handler on dispose 2018-06-21 14:14:36 +10:00
Daniel Imms 27b1c89f43 More cleaning up of references 2018-06-21 11:48:53 +10:00
Daniel Imms 867ce262e8 Fix test 2018-06-21 11:15:10 +10:00
Daniel Imms 45e4486bf5 Remove on, off, globalOn 2018-06-21 11:10:55 +10:00
Daniel Imms 545d0782c9 Properly register and dispose of majority of remaining listeners 2018-06-21 11:07:29 +10:00
Daniel Imms ede1d5f7c8 Correctly dispose of Renderer and ScreenDprMonitor 2018-06-21 10:49:03 +10:00
Daniel Imms 5adf962eb5 Dispose of attach addon event listeners 2018-06-21 10:39:52 +10:00
Daniel Imms 9293616f02 Add dispose terminal button to demo 2018-06-21 10:32:58 +10:00
Daniel Imms 7e779cd195 Replace _disposables with Disposable, add jsdoc 2018-06-21 10:29:02 +10:00
Daniel Imms dfd17be8bb Introduce Disposable abstract class 2018-06-21 10:24:37 +10:00
Daniel Imms a44f496ec7 Rename dom listener function for better clarity 2018-06-21 10:05:40 +10:00
Daniel Imms c0f4a8eb7c Move Dom listener helper to ui/
Part of #1518
2018-06-21 10:03:53 +10:00
Daniel Imms 086b2c04e0 Make sure DOM renderer doesn't render cells beyond cols
Fixes #1523
2018-06-20 23:02:05 +10:00
PowerHat 4809b04f26 Merge branch 'master' into master 2018-06-20 14:51:52 +02:00
7PH 4fecf05dab #1521: added unit tests for saving & restoring cursor attributes 2018-06-20 14:31:12 +02:00
7PH 285b0b6702 #1521: added 'savedCurAttr' attribute to 'MockInputHandlingTerminal' class 2018-06-20 14:23:05 +02:00
Benjamin Raymond a1b8f96d50 #1521: added default behavior of 'ESC 8' escape sequence (DECRC) when cursor state was never saved before using 'ESC 7' (DECSC) 2018-06-20 14:04:13 +02:00
Benjamin Raymond c345dba591 #1521: added 'savedCurAttr' property in 'Terminal' class which holds the saved state of the character attributes ('ESC 7' escape sequence code) 2018-06-20 13:58:53 +02:00
Benjamin Raymond 8dcb4698d3 #1521: now saving and restoring characters attributes when using ANSI escape sequences 'ESC 7' (DECSC) and 'ESC 8' (DECRC) 2018-06-20 13:57:42 +02:00
Daniel Imms f9bc3c511b Merge pull request #1508 from Tyriar/layering
Start of improved layering
2018-06-20 21:35:51 +10:00
Daniel Imms 0d0f4b048c Merge branch 'master' into layering 2018-06-20 21:25:58 +10:00
Daniel Imms 91cf9e1ba8 Move .strings to public/ 2018-06-20 19:46:04 +10:00
Daniel Imms 5d05c3a5f3 Merge pull request #1517 from JonKohler/patch-1
Sort asending for companies in readme and add Nutanix
2018-06-20 07:40:13 +10:00
Jon Kohler 628e648b57 Update README.md
Update readme and add note for future adders to append to the bottom of list
2018-06-18 22:28:25 -04:00
Jon Kohler 72bb1b030a Sort asending for companies in readme and add Nutanix
Sorted the company list in the readme asending and added Nutanix to the list of users
2018-06-18 10:05:40 -04:00
Daniel Imms 6c6d90a698 Merge branch 'master' into layering 2018-06-13 12:52:43 +02:00
Daniel Imms 356959e338 Merge pull request #1512 from Tyriar/1511_npe_beginFrame
Add a null check to charatlas beginFrame call
2018-06-12 18:02:21 +02:00
Daniel Imms 6c3b6b4655 Ensure drawBoldTextInBrightColors redraws screen 2018-06-12 17:54:26 +02:00
Daniel Imms ee935cc68c Add a null check to charatlas beginFrame call
Fixes #1511
2018-06-12 17:27:36 +02:00
Daniel Imms 57959249d8 Resolve todos 2018-06-12 17:17:21 +02:00
Daniel Imms bb4db71958 Rename base/ to common/ 2018-06-12 17:07:43 +02:00
Daniel Imms 8c3376911f Fix winptyCompat compile 2018-06-12 17:05:13 +02:00
Daniel Imms 1487b8d02d Merge pull request #1510 from FGasper/cpanel_whm_real_world
Add cPanel & WHM to the Real-World Uses list.
2018-06-12 10:25:10 +02:00
Felipe Gasper 3705d8159d Add cPanel & WHM to the Real-World Uses list. 2018-06-11 16:51:07 -04:00
Daniel Imms 6a3ed43ad2 Move EscapeSequences into base 2018-06-10 16:56:29 +01:00
Daniel Imms 12e7f80037 Move Charsets to core 2018-06-10 16:53:28 +01:00
Daniel Imms 410d506f34 Fix applyAddon test 2018-06-10 16:44:50 +01:00
Daniel Imms d29a527724 Introduce the public module 2018-06-10 16:40:36 +01:00
Daniel Imms 20307ef637 Add tests for mobile key events 2018-06-10 12:43:22 +02:00
Daniel Imms 95217ca931 Remove core/input/Keyboard's dep on Terminal 2018-06-10 12:33:50 +02:00
Daniel Imms b4ccbe042b Pull keyboard event translation into core 2018-06-10 11:52:25 +02:00
Daniel Imms e167686b6d Remove conversion step, KeyboardEvent implements IKeyboardEvent 2018-06-10 11:45:50 +02:00
Daniel Imms b6481f120b Introduce IKeyEvent 2018-06-10 11:03:37 +02:00
Daniel Imms fb63b3475b Merge pull request #1501 from Tyriar/issue_template
Add more granular github issue templates
2018-06-09 17:02:36 +02:00
Daniel Imms 2f1c1cd172 Remove old issue template 2018-06-09 17:01:37 +02:00
Daniel Imms 03e31571a9 Merge pull request #1497 from xtermjs/Tyriar-patch-1
Fix copyright line breaks
2018-06-09 17:00:24 +02:00
Daniel Imms c6370ba0cf Merge pull request #1472 from Tyriar/fix_tsconfig
Fix valid lib values in tsconfig
2018-06-09 17:00:05 +02:00
Daniel Imms 432c2cc369 Nullify and remove mouse handlers when mouse move it exited
Fixes #998
2018-06-08 17:36:04 +02:00
Daniel Imms 3637b83bbb Add more granular github issue templates 2018-06-08 09:04:37 +02:00
Daniel Imms ddc9466916 Merge pull request #1500 from Tyriar/coc
Add a code of conduct
2018-06-08 08:55:21 +02:00
Daniel Imms df9d20bfdc Add a code of conduct 2018-06-08 08:54:29 +02:00
Daniel Imms e9af4d0e80 Fix copyright line breaks 2018-06-07 09:22:14 +02:00
Daniel Imms f38a34c6da Merge pull request #1495 from nikonso/passive-events
Explicitly  mark Event Listeners as not passive
2018-06-06 10:04:39 +02:00
Daniel Imms 41b5fd8ddf Merge branch 'master' into passive-events 2018-06-06 09:57:42 +02:00
Daniel Imms 22ef5661eb Merge pull request #1388 from Tyriar/script_improvements
Improved test related npm scripts
2018-06-06 09:23:20 +02:00
Daniel Imms 4e2c30c81d Pin @types/jsdom 2018-06-06 09:09:40 +02:00
Bruno Ribeito 8f91e3bcde Fix #829 2018-06-06 08:02:26 +01:00
Daniel Imms 5aff7c2c1e Update @types/jsdom 2018-06-06 08:47:08 +02:00
Daniel Imms b4bb433c1b Add coveralls 2018-06-06 07:44:09 +02:00
Daniel Imms c405b58456 Merge branch 'master' into script_improvements 2018-06-06 05:45:16 +02:00
Daniel Imms 445689c679 Merge pull request #1494 from svenstaro/patch-1
Add genact
2018-06-06 04:23:48 +02:00
Sven-Hendrik Haase 1c0d7771d7 Add genact
genact uses xterm.js!
2018-06-05 20:58:39 +02:00
Daniel Imms a8964a15dd Merge pull request #1491 from pro-src/master
Fix typo in README.md
2018-06-05 08:36:42 +02:00
pro-src bdfdcf3508 Fix typo in README.md 2018-06-04 14:39:47 -05:00
Daniel Imms 0276ab4f85 Merge pull request #1489 from nikonso/master
Fix npm start on windows
2018-06-03 19:10:16 -07:00
Daniel Imms f83394b0ab Merge pull request #1490 from nikonso/fix-demo-string-options
Fix Demo String Options
2018-06-03 19:09:23 -07:00
Bruno Ribeito 13d5ad6506 Fix Demo String Options 2018-06-03 19:53:51 +01:00
Bruno Ribeito 7751933c8c Fix npm start on windows 2018-06-03 19:37:49 +01:00
Daniel Imms 42d7d3aefc Merge pull request #1488 from Tyriar/all_options
Add all options to demo in a way that makes it easy to maintain
2018-06-03 07:51:14 -07:00
Daniel Imms 0f488e59d7 Merge branch 'master' into all_options 2018-06-03 07:43:27 -07:00
Daniel Imms 1066816142 Merge pull request #1474 from Tyriar/1473_slow_build
Use incremental tsc builds
2018-06-03 07:42:27 -07:00
Daniel Imms 6e3c16a2c2 Merge branch 'master' into 1473_slow_build 2018-06-03 07:39:19 -07:00
Daniel Imms 3d56b152f3 Merge pull request #1421 from Tyriar/more_naming_conventions
Add remaining naming conventions into tslint
2018-06-03 07:39:03 -07:00
Daniel Imms 24fad1126c Add missing renames 2018-06-03 16:34:12 +02:00
Daniel Imms 53463681de Fix lint 2018-06-03 16:25:32 +02:00
Daniel Imms 2aff2eb710 Merge branch 'master' into more_naming_conventions 2018-06-03 07:23:14 -07:00
Daniel Imms c748b02111 Add rendererType 2018-06-03 16:21:23 +02:00
Daniel Imms 73e35b9293 Merge branch 'master' into all_options 2018-06-03 07:19:51 -07:00
Daniel Imms 1cccec6398 Merge pull request #1432 from Tyriar/1360_dom_renderer
Implement a fallback DOM renderer
2018-06-03 07:19:33 -07:00
Daniel Imms 1ca6e51564 Fix lint 2018-06-03 16:06:45 +02:00
Daniel Imms 52be55852e Merge branch 'master' into 1360_dom_renderer 2018-06-03 06:58:08 -07:00
Daniel Imms 824a03e0af Merge branch 'master' into 1473_slow_build 2018-06-03 06:43:42 -07:00
Daniel Imms 3c02c9ce66 Merge pull request #1457 from Tyriar/vsts
Add VSTS build badge
2018-06-03 06:41:50 -07:00
Daniel Imms adb0cf41ab Improve display of options 2018-06-03 15:37:01 +02:00
Daniel Imms de683b35b2 Merge pull request #1485 from Tyriar/664_fit_readme
Add basic fit readme
2018-06-03 06:13:30 -07:00
Daniel Imms 767e3570ac Merge pull request #1486 from Tyriar/1456_triple_click_wrapped
Select entire wrapped line on triple click
2018-06-03 06:10:22 -07:00
Daniel Imms 75a45808c3 Merge branch 'master' into 1456_triple_click_wrapped 2018-06-03 06:04:41 -07:00
Daniel Imms 7ca6873a5e Merge pull request #1487 from Tyriar/no_implicit_any
Enable noImplicitAny compiler option
2018-06-03 06:03:37 -07:00
Daniel Imms 908b06cfa7 Some polish 2018-06-03 11:18:34 +01:00
Daniel Imms 2d786910c8 Correctly set cols/rows 2018-06-03 11:10:11 +01:00
Daniel Imms 5b04451b05 console error on setting ctor only option via setOption 2018-06-03 11:02:39 +01:00
Daniel Imms 4d5e305a25 Generate inputs for all options in demo 2018-06-03 10:37:20 +01:00
Daniel Imms 4dff828374 Add @types/glob 2018-06-03 07:15:45 +01:00
Daniel Imms fea8ba03b8 Finish removing all implicit anys 2018-06-02 19:02:54 -07:00
Daniel Imms 918ee16f86 Don't set options on terminaol object itself 2018-06-02 18:47:04 -07:00
Daniel Imms b79ab7212c Remove a bunch of implicit anys 2018-06-02 18:39:05 -07:00
Daniel Imms 956b773421 Add more tests, fix boundary bug 2018-06-02 18:07:49 -07:00
Daniel Imms cd3418517e Select the whole wrapped line when triple clicking
Fixes #1456
2018-06-02 17:56:44 -07:00
Daniel Imms 285911cd98 Implement IBuffer.getWrappedRangeForLine
Part of #1456
2018-06-02 17:53:35 -07:00
Daniel Imms 5acc45f08b Add basic fit readme
Fixes #664
2018-06-02 12:22:05 -07:00
Daniel Imms c192a27d93 Merge pull request #1483 from AndrienkoAleksandr/littleFix
Minor: clean up build logs for source code without tests.
2018-06-01 09:12:05 -07:00
Daniel Imms d5af79fc5f Include typings in addons as well 2018-06-01 08:55:22 -07:00
Oleksandr Andriienko 90b9a487cd Minor: clean up build logs for source code without tests.
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-06-01 13:41:35 +03:00
Daniel Imms 4617bab3a0 Merge pull request #1477 from lgeiger/let-const
Prefer const vs let for variables that are only declared once
2018-05-30 11:31:24 -07:00
Lukas Geiger 8989a09853 Prefer const vs let for variables that are only declared once
Requires that variable declarations use `const` instead of `let` and `var` if possible.

This is mostly for code readability, performance wins are debatable.
2018-05-30 19:47:28 +02:00
Daniel Imms 4a4a17966a Merge pull request #1475 from cathy-zhao/master
add a project to real-world uses list
2018-05-29 06:24:24 -07:00
赵雪珂 7db0b181bc add a project to real-world uses list 2018-05-29 13:03:48 +08:00
Daniel Imms 3ce71b7f8f Add css copy tasks 2018-05-28 05:49:47 -07:00
Daniel Imms c2568a6857 Fix Windows build 2018-05-28 05:44:15 -07:00
Daniel Imms 834b266b7f Move demo build into npm start 2018-05-27 07:59:44 -07:00
Daniel Imms 5d9b761f33 Merge branch 'master' into 1473_slow_build 2018-05-27 07:57:48 -07:00
Daniel Imms 35fc10b0e6 Kill watch/prebuild tasks when one fails 2018-05-27 07:42:34 -07:00
Daniel Imms ffd7d5513b Rebuild demo when demo files change 2018-05-27 07:38:04 -07:00
Daniel Imms 656567aed0 Use incremental tsc builds 2018-05-27 07:30:37 -07:00
Daniel Imms 82fdcc0771 Get DomRendererRowFactory test coverage to 100% 2018-05-26 08:43:55 -07:00
Daniel Imms c2b5a243f2 Refactor and add some tests to dom renderer 2018-05-26 08:15:40 -07:00
Daniel Imms 6e9cd35176 Remove coverage files from git 2018-05-26 07:34:32 -07:00
Daniel Imms 9890a8b20f Fix valid lib values in tsconfig
Mixed case is not valid
2018-05-26 07:32:46 -07:00
Daniel Imms 20975278c5 Merge remote-tracking branch 'ups/master' into 1360_dom_renderer 2018-05-26 07:25:08 -07:00
Daniel Imms b43ee08dd2 Merge pull request #1469 from tmpfs/safari-create-image-bitmap-patch
Suppress Safari promise rejections.
2018-05-23 20:28:06 -07:00
muji 3ae96a2266 Improve isSafari test. 2018-05-24 10:18:43 +08:00
muji 87fee8d024 Suppress Safari promise rejections.
Currently Webkit implements createImageBitmap() but returns a promise
rejection.

Note that once Webkit implements createImageBitmap() correctly these
changes should be removed.
2018-05-24 10:11:03 +08:00
Daniel Imms 09e42af730 Merge pull request #1466 from jerch/cleanup_print
cleanup and performance optimizations for InputHandler.print
2018-05-22 06:45:11 -07:00
Daniel Imms 44fde26f04 Remove types when they're inferred 2018-05-22 06:44:50 -07:00
Jörg Breitbart ac9b71549a add missing type decl 2018-05-21 22:39:00 +02:00
Jörg Breitbart 328086f206 declare types in InputHandler.print 2018-05-21 22:29:05 +02:00
Jörg Breitbart b3e8197111 changes in InputHandler.print:
- call updateRange only at start and end
- make all repeated terminal accesses local
- set full charCode (codepoint) in terminal buffer
2018-05-21 20:58:23 +02:00
Daniel Imms 099c7d8f04 Merge pull request #1399 from jerch/new_parser
new parser
2018-05-21 10:48:03 -07:00
Daniel Imms 507c4dc5d2 Use nyc for coverage report
Fixes #1464
2018-05-21 10:42:28 -07:00
Daniel Imms 6bc6ea56a5 Merge branch 'master' into new_parser 2018-05-21 10:04:15 -07:00
Jörg Breitbart e97882e5ad use string templates in DCS responses 2018-05-21 18:52:27 +02:00
Daniel Imms 85dada2eed Merge remote-tracking branch 'origin/master' into 1360_dom_renderer 2018-05-21 09:41:21 -07:00
Jörg Breitbart 8386ec1c19 Merge branch 'new_parser' of https://github.com/jerch/xterm.js into new_parser 2018-05-21 18:34:51 +02:00
Jörg Breitbart ceb1432f75 remove deprecated FIXME 2018-05-21 18:33:59 +02:00
Daniel Imms 8d722d20e2 Merge pull request #1463 from Tyriar/1424_fix_static_cache_ghost
Save and restore correctly when drawing from static cache
2018-05-21 09:21:58 -07:00
Daniel Imms 054abd15ea Save and restore correctly when drawing from static cache
Fixes #1424
2018-05-21 09:11:41 -07:00
Daniel Imms 724aa9d6c7 Fix compile and lint 2018-05-21 08:17:07 -07:00
Daniel Imms ab5fc98627 Merge branch 'master' into more_naming_conventions 2018-05-21 08:06:52 -07:00
Daniel Imms 0dfc9b2118 Merge branch 'master' into vsts 2018-05-21 07:54:50 -07:00
Daniel Imms 8bbc1fa287 Merge branch 'master' into script_improvements 2018-05-21 07:54:07 -07:00
Daniel Imms c1f8225c4f Merge branch 'master' into new_parser 2018-05-21 07:30:35 -07:00
Daniel Imms 1736c1e12d Prevent outside line-height styles from affecting cursor
See Microsoft/vscode#50125
2018-05-18 12:17:39 -07:00
Daniel Imms c00a3726f3 Merge branch 'master' into vsts 2018-05-16 13:46:06 -07:00
Daniel Imms 5ef399df6f Add VSTS build badge 2018-05-16 13:44:46 -07:00
Daniel Imms f8e9d02566 Merge branch 'master' into more_naming_conventions 2018-05-16 09:45:42 -07:00
Daniel Imms d8afdb1b6b Merge branch 'master' into new_parser 2018-05-15 13:21:33 -07:00
Daniel Imms 2b4c5adb3c Set font family and size 2018-05-15 11:29:51 -07:00
Daniel Imms 6024e7c5fe Size row container so scroll bar shows 2018-05-15 10:52:01 -07:00
Daniel Imms aa1e1c5d94 Scope styles per terminal 2018-05-15 10:18:43 -07:00
Daniel Imms f055e0ef03 Throw exception for unrecognized renderer type 2018-05-15 10:10:34 -07:00
Daniel Imms 2f618b37c1 Merge branch 'master' into 1360_dom_renderer 2018-05-15 10:08:09 -07:00
Jörg Breitbart 02893dcbb8 fix some docs 2018-05-07 23:28:40 +02:00
Jörg Breitbart a37449a8cd some docs cleanup 2018-05-06 23:33:52 +02:00
Jörg Breitbart 551ccc0f2f explicit arguments for fallback handlers 2018-05-06 23:02:54 +02:00
Jörg Breitbart 108aa5ab31 test Uint8Array and Array; fix Uint8Array type 2018-05-06 22:42:07 +02:00
Daniel Imms 362d7698f7 Some polish 2018-05-05 08:12:52 -07:00
Daniel Imms edb06c6b2a Add copyright header 2018-05-05 07:54:38 -07:00
Jörg Breitbart 6cefb4c634 more test cases 2018-05-05 16:26:07 +02:00
Daniel Imms cd3eea5b48 Improve typings 2018-05-04 11:34:24 -07:00
Daniel Imms 5b5eb48867 Polish 2018-05-04 10:55:41 -07:00
Daniel Imms 09624c18e0 Move standard width to style element 2018-05-04 10:43:54 -07:00
Daniel Imms 52e71a2530 Support italic 2018-05-04 10:13:21 -07:00
Daniel Imms 6cadd7c4a8 Fill in documentation on new option 2018-05-04 10:02:01 -07:00
Daniel Imms b14308f2c4 Resolve a bunch of TODOs 2018-05-04 09:38:24 -07:00
Daniel Imms 2c3949375e Update after a options change 2018-05-04 08:37:41 -07:00
Daniel Imms e3e4dcb5d1 Support selection 2018-05-04 08:32:03 -07:00
Daniel Imms e9f4d39f2b Start on selection 2018-05-04 06:27:40 -07:00
Daniel Imms d777513b87 Handle focus, emit refresh 2018-05-03 11:58:46 -07:00
Daniel Imms 1d2f356cfd Add a bunch of constants 2018-05-03 11:44:46 -07:00
Daniel Imms 418f2c6cdf Fix row height, add cursor 2018-05-03 11:35:04 -07:00
Daniel Imms 3cab8bede1 Merge branch 'oncharsizechanged' into 1360_dom_renderer 2018-05-02 15:05:42 -07:00
Daniel Imms 58ce5e2110 Only update dimensions when char size changes 2018-05-02 15:05:37 -07:00
Daniel Imms 20ff107768 Call IRenderer.onCharSizeChanged at right time
Even though Renderer just defers this to onResize, future IRenderer
implementations may take advantage of onCharSizeChanged specifically.
2018-05-02 15:00:42 -07:00
Daniel Imms 16393b228d Initial DOM renderer implementation 2018-05-02 13:32:22 -07:00
Jörg Breitbart 0f8d22547e fix identation 2018-05-02 01:23:46 +02:00
Jörg Breitbart c069e94a4c Merge branch 'master' into new_parser 2018-05-02 00:42:23 +02:00
Jörg Breitbart a1a4cac113 dcs handler implementations 2018-05-02 00:22:35 +02:00
Jörg Breitbart 25ff77af03 fix transition definitions 2018-05-01 22:22:40 +02:00
Jörg Breitbart 536b3a8def fix member scope and tests 2018-05-01 22:17:26 +02:00
Jörg Breitbart d090cbe4b9 some docs 2018-05-01 21:54:15 +02:00
Jörg Breitbart 9a32852e61 single arg for DCS and ESC handler registration 2018-05-01 21:07:13 +02:00
Jörg Breitbart 89cf45c514 save func pointer in local var 2018-05-01 20:51:36 +02:00
Jörg Breitbart 0c200f2feb remove collect from args where not needed 2018-05-01 20:41:41 +02:00
Jörg Breitbart 728eeebcc7 set member through ctor argument 2018-05-01 20:33:48 +02:00
Jörg Breitbart c7fdff2f40 several changes:
- missing escape sequence related methods in InputHandler implemented
- created namespace for C1 control codes
2018-05-01 17:50:41 +02:00
Jörg Breitbart 2eeb7b8ea5 ESC and OSC method entries for InputHandler 2018-05-01 16:36:12 +02:00
Jörg Breitbart 0535f5d1b3 unified method signature in InputHandler, moved some CSI condition into methods (FIXME: needs intermediate handler) 2018-05-01 14:58:16 +02:00
Jörg Breitbart aeae688b4c multiple changes in InputHandler:
- remove prefix/postfix
- optional collect arg where needed
- tempPrefix handler removed
- replaced .bind with () => notation
- moved callback registering to ctor again
2018-05-01 14:32:08 +02:00
Jörg Breitbart af392a6305 cleanup, some docs, temp fix for InputHandler tests 2018-04-30 23:35:54 +02:00
Jörg Breitbart ba6fcddc43 real merge of InputHandler and ParserTerminal 2018-04-30 17:04:29 +02:00
Jörg Breitbart dbc6b12ec2 missing whitespace 2018-04-30 15:44:21 +02:00
Jörg Breitbart 645cd2eb2c fix test cases 2018-04-30 15:38:17 +02:00
Jörg Breitbart b158176862 local .buffer in parse 2018-04-30 14:25:29 +02:00
Jörg Breitbart a1035269d5 add array fallback for TransitionTable 2018-04-30 11:27:10 +02:00
Jörg Breitbart f5375c3a99 fix error in esc handler call 2018-04-30 02:06:57 +02:00
Jörg Breitbart a55a7f3695 some cleanup 2018-04-30 01:49:29 +02:00
Jörg Breitbart 826cca7088 fix import issue 2018-04-30 01:32:49 +02:00
Jörg Breitbart 0f3d82a2a5 faster print action 2018-04-29 23:48:30 +02:00
Jörg Breitbart dc14f5226d fix InputHandler 2018-04-29 19:34:43 +02:00
Jörg Breitbart 27a3347c3f Merge branch 'master' into new_parser 2018-04-29 19:30:35 +02:00
Jörg Breitbart 2d77fcf2fb merge ParserTerminal with InputHandler
- quick'n dirty merge of classes - ParserTerminal is subclass of InputHandler
- basic fallback handlers implemented
- test regressions due to multiple changes in EscapeSequenceParser
- runtime down to 2.3s
- TODO: fix tests, implement DCS functions, full merge of classes
2018-04-29 19:10:47 +02:00
Jörg Breitbart 9d2bbd199c remove term from EscapeSequenceParser 2018-04-29 17:23:38 +02:00
Jörg Breitbart b72cdadd11 move switches to jump tables 2018-04-29 15:48:19 +02:00
Daniel Imms aaaec75d73 Add remaining naming conventions into tslint 2018-04-26 13:54:20 -07:00
Jörg Breitbart 9eb107a6e9 first handler API changes 2018-04-24 18:53:19 +02:00
Jörg Breitbart c0ab2b9a7c rename enums 2018-04-24 13:06:03 +02:00
Jörg Breitbart a058395014 move addChar to ParserTerminal 2018-04-24 12:27:43 +02:00
Jörg Breitbart 55993ada9b merge with remote 2018-04-24 00:35:55 +02:00
Jörg Breitbart c8910cc5fe utilize more typescript features 2018-04-24 00:23:46 +02:00
Jörg Breitbart f9767073ba new parser 2018-04-24 00:21:27 +02:00
Jörg Breitbart 20760d09e4 utilize more typescript features 2018-04-22 04:34:42 +02:00
Jörg Breitbart 73f40ef7d2 merge with master 2018-04-21 16:04:17 +02:00
Jörg Breitbart 9b085d2bdd new parser 2018-04-21 16:02:46 +02:00
Jörg Breitbart fb7e5d590c new parser 2018-04-21 05:12:24 +02:00
Daniel Imms 3010760352 Remove vscode settings
No longer required as all the folders are in gitignore
2018-04-13 11:37:52 -07:00
Daniel Imms 82c16dee73 Rename coverage to test-coverage and document 2018-04-13 10:53:45 -07:00
Daniel Imms 7e8dd9647f Add testing section to README 2018-04-13 10:49:02 -07:00
Daniel Imms b594641b26 Run coverage on travis ci 2018-04-13 10:41:27 -07:00
Daniel Imms 5df9808d4c Remove launch.json in favor of auto attach debugging 2018-04-13 10:41:12 -07:00
100 changed files with 5492 additions and 2273 deletions
-11
View File
@@ -1,11 +0,0 @@
<!-- Enter your issue description here -->
## Details
- Browser and browser version:
- OS version:
- xterm.js version:
### Steps to reproduce
1.
2.
+17
View File
@@ -0,0 +1,17 @@
---
name: Bug report
about: Create a bug report
---
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
<!-- Describe the bug here. -->
## Details
- Browser and browser version:
- OS version:
- xterm.js version:
### Steps to reproduce
1.
2.
@@ -0,0 +1,7 @@
---
name: Feature request
about: Suggest a feature or enhancement
---
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
<!-- Describe the enhancement here. -->
+8
View File
@@ -0,0 +1,8 @@
---
name: Question
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
---
🛑 The issue tracker is not for questions 🛑
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
+2 -1
View File
@@ -18,7 +18,8 @@ fixtures/typings-test/*.js
package-lock.json
# Directories needed for code coverage
/coverage/
coverage/
.nyc_output/
# Keep bundled code out of Git
dist/
+1 -1
View File
@@ -13,7 +13,7 @@ env:
matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint
- NPM_COMMAND=test
- NPM_COMMAND=test-coverage
notifications:
email: false
script: npm run $NPM_COMMAND
-23
View File
@@ -1,23 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Mocha Tests",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
"windows": {
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
},
"runtimeArgs": [
"--colors",
"--recursive",
"${workspaceRoot}/lib"
],
"sourceMaps": true,
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
"internalConsoleOptions": "openOnSessionStart"
}
]
}
-8
View File
@@ -1,8 +0,0 @@
{
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
"**/build": true,
"**/lib": true
}
}
+10
View File
@@ -1,5 +1,7 @@
List of xterm.js contributors. Updated before every release.
7PH <b.raymond@cern.ch>
7PH <b.raymond@protonmail.com>
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
@@ -19,6 +21,7 @@ Austin Robertson <austinrobertson@gmail.com>
ayapi <colors.aya@gmail.com>
Ben Hall <ben@benhall.me.uk>
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Benjamin Raymond <b.raymond@cern.ch>
Benjamin Woodruff <github@benjam.info>
Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
@@ -64,6 +67,7 @@ Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Austin <jon.i.austin@gmail.com>
Jon Kohler <jon@nutanix.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
jpoth <poth.john@gmail.com>
@@ -76,6 +80,7 @@ Linus Unnebäck <linus@folkdatorn.se>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Lukas Geiger <lukas.geiger94@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Marek Libra <mlibra@redhat.com>
@@ -88,6 +93,7 @@ Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
muji <muji@tmpfs.org>
Nicolas Ramz <nicolas.ramz@gmail.com>
npezza93 <npezza93@gmail.com>
Oleksandr Andriienko <oandriie@redhat.com>
@@ -96,6 +102,7 @@ Paris Kasidiaris <paris@sourcelair.com>
Peng Xiao <pengxiao@outlook.com>
Peter Baumgarten <me@peterbaumgarten.com>
Philip Olson <philip.olson@protonmail.ch>
PowerHat <31401273+7PH@users.noreply.github.com>
pro-src <34285059+pro-src@users.noreply.github.com>
pro-src <rodneyd.teal@gmail.com>
Rick Baker <rick@ricktbaker.com>
@@ -108,6 +115,7 @@ Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
Sven-Hendrik Haase <svenstaro@gmail.com>
t-amqi <umberos@gmail.com>
Thanasis Daglis <thanasis@sourcelair.com>
thomas <thomas@kreativkonzentrat.de>
@@ -115,6 +123,8 @@ Thomas Zilz <t.zilz@mofux.org>
Tine Jozelj <tine.jozelj@outlook.com>
Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
Viraj Sinha <virajosinha@gmail.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
赵雪珂 <zhaoxueke@onething.net>
+73
View File
@@ -0,0 +1,73 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting [@Tyriar](https://twitter.com/Tyriar) or [@pariskasid](https://twitter.com/pariskasid) on Twitter. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
+20 -6
View File
@@ -1,6 +1,6 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
[![Travis CI build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
@@ -39,7 +39,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<body>
<div id="terminal"></div>
<script>
var term = new Terminal();
var term = new Terminal();
term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
@@ -149,8 +149,12 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
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.
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. Note: Please add any new contributions to the end of the list only.
## Demo
@@ -223,6 +227,16 @@ 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.*
## Testing
Tests are run using the following npm scripts:
- `npm test`: This will run both unit tests and the linter
- `npm run test-suite <file>`: This will run all tests within a particular file, &lt;file&gt; is the test file name excluding the extension (eg. "Linkifier.test")
- `npm run test-debug`: This will run unit tests with `--inspect-brk`, this can then be automatically debugged using [VS Code auto attach](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_auto-attach-feature) or manually attached to by a debugger
- `npm run test-coverage`: This will run all unit tests and produce a coverage report
- `npm run lint`: This will run the linter only
## Releases
Xterm.js follows a monthly release cycle roughly.
@@ -237,12 +251,12 @@ To contribute either code, documentation or issues to xterm.js please read the [
### 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.
`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 ran when 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) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+35 -92
View File
@@ -1,95 +1,38 @@
<!doctype html>
<html>
<head>
<title>xterm.js demo</title>
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
</head>
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
<div id="terminal-container"></div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
</p>
<p>
<label><input type="checkbox" id="option-transparency"> transparency</label>
</p>
<p>
<label>
cursorStyle
<select id="option-cursor-style">
<option value="block">block</option>
<option value="underline">underline</option>
<option value="bar">bar</option>
</select>
</label>
</p>
<p>
<label>
bellStyle
<select id="option-bell-style">
<option value="">none</option>
<option value="sound">sound</option>
<option value="visual">visual</option>
<option value="both">both</option>
</select>
</label>
</p>
<p>
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
</p>
<p>
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
</p>
<p>
<label>
experimentalCharAtlas
<select id="option-experimental-char-atlas">
<option value="static" selected>static</option>
<option value="dynamic">dynamic</option>
<option value="none">none</option>
</select>
</label>
</p>
<div>
<h3>Size</h3>
<div>
<div style="display: inline-block; margin-right: 16px;">
<label for="cols">Columns</label>
<input type="number" id="cols" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<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>
<h3>Accessibility</h3>
<p>
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
</p>
</div>
<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>
<head>
<title>xterm.js demo</title>
<link rel="stylesheet" href="/build/xterm.css" />
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
<link rel="stylesheet" href="style.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
</head>
<body>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
<div id="terminal-container"></div>
<div>
<h3>Actions</h3>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h3>Options</h3>
<p>These options can be set in the <code>Terminal</code> constructor or using the <code>Terminal.setOption</code> function.</p>
<div id="options-container"></div>
</div>
<div>
<h3>Style</h3>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
<hr/>
<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>
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+147 -83
View File
@@ -26,97 +26,38 @@ var terminalContainer = document.getElementById('terminal-container'),
findNext: document.querySelector('#find-next'),
findPrevious: document.querySelector('#find-previous')
},
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'),
transparency: document.querySelector('#option-transparency'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'),
bellStyle: document.querySelector('#option-bell-style'),
screenReaderMode: document.querySelector('#option-screen-reader-mode')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows'),
paddingElement = document.getElementById('padding');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
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.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") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
actionElements.findPrevious.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
optionElements.macOptionIsMeta.addEventListener('change', function () {
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
});
optionElements.transparency.addEventListener('change', function () {
var checked = optionElements.transparency.checked;
term.setOption('allowTransparency', checked);
term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {});
});
optionElements.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
optionElements.experimentalCharAtlas.addEventListener('change', function () {
term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value);
});
optionElements.screenReaderMode.addEventListener('change', function () {
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
});
createTerminal();
const disposeRecreateButtonHandler = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
term.dispose();
term = null;
window.term = null;
socket = null;
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
}
else {
createTerminal();
document.getElementById('dispose').innerHTML = 'Dispose terminal';
}
};
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
function createTerminal() {
// Clean terminal
while (terminalContainer.children.length) {
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),
screenReaderMode: optionElements.screenReaderMode.checked
});
term = new Terminal({});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) {
if (!pid) {
@@ -137,14 +78,30 @@ function createTerminal() {
term.fit();
term.focus();
addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findNext(actionElements.findNext.value);
}
});
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
if (e.key === "Enter") {
e.preventDefault();
term.findPrevious(actionElements.findPrevious.value);
}
});
// fit is called within a setTimeout, cols and rows need this.
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
initOptions(term);
document.getElementById(`opt-cols`).value = term.cols;
document.getElementById(`opt-rows`).value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
updateTerminalSize();
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
@@ -184,7 +141,7 @@ function runFakeTerminal() {
term.writeln('');
term.prompt();
term.on('key', function (key, ev) {
term._core.register(term.addDisposableListener('key', function (key, ev) {
var printable = (
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
);
@@ -199,9 +156,116 @@ function runFakeTerminal() {
} else if (printable) {
term.write(key);
}
}));
term._core.register(term.addDisposableListener('paste', function (data, ev) {
term.write(data);
}));
}
function initOptions(term) {
var blacklistedOptions = [
// Internal only options
'cancelEvents',
'convertEol',
'debug',
'handler',
'screenKeys',
'termName',
'useFlowControl',
// Complex option
'theme',
// Only in constructor
'rendererType'
];
var stringOptions = {
bellSound: null,
bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'],
experimentalCharAtlas: ['none', 'static', 'dynamic'],
fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
};
var options = Object.keys(term._core.options);
var booleanOptions = [];
var numberOptions = [];
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
switch (typeof term.getOption(o)) {
case 'boolean':
booleanOptions.push(o);
break;
case 'number':
numberOptions.push(o);
break;
default:
if (Object.keys(stringOptions).indexOf(o) === -1) {
console.warn(`Unrecognized option: "${o}"`);
}
}
});
term.on('paste', function (data, ev) {
term.write(data);
var html = '';
html += '<div class="option-group">';
booleanOptions.forEach(o => {
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
});
html += '</div><div class="option-group">';
numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
});
html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => {
if (stringOptions[o]) {
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
} else {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
}
});
html += '</div>';
var container = document.getElementById('options-container');
container.innerHTML = html;
// Attach listeners
booleanOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.checked);
term.setOption(o, input.checked);
});
});
numberOptions.forEach(o => {
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
} else {
term.setOption(o, parseInt(input.value, 10));
}
});
});
Object.keys(stringOptions).forEach(o => {
var input = document.getElementById(`opt-${o}`);
addDomListener(input, 'change', () => {
console.log('change', o, input.value);
term.setOption(o, input.value);
});
});
}
function addDomListener(element, type, handler) {
element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
}
function updateTerminalSize() {
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.fit();
}
+16
View File
@@ -14,3 +14,19 @@ h1 {
margin: 0 auto;
padding: 2px;
}
p {
font-size: 0.9em;
font-style: italic
}
#option-container {
display: flex;
justify-content: center;
}
.option-group {
display: inline-block;
padding-left: 20px;
vertical-align: top;
}
+24 -80
View File
@@ -4,11 +4,9 @@
const browserify = require('browserify');
const buffer = require('vinyl-buffer');
const coveralls = require('gulp-coveralls');
const fs = require('fs-extra');
const gulp = require('gulp');
const path = require('path');
const istanbul = require('gulp-istanbul');
const merge = require('merge-stream');
const mocha = require('gulp-mocha');
const sorcery = require('sorcery');
@@ -20,69 +18,37 @@ const webpack = require('webpack-stream');
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const addons = fs.readdirSync(`${__dirname}/src/addons`);
const TEST_PATHS = [
`${outDir}/*test.js`,
`${outDir}/**/*test.js`,
`${outDir}/*integration.js`,
`${outDir}/**/*integration.js`
];
// 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) {
outDir = `${__dirname}/${path.normalize(outDir)}`;
}
/**
* Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript
* file compiled.
*/
gulp.task('tsc', function () {
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/
fs.emptyDirSync(`${outDir}`);
gulp.task('css', function() {
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
});
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in
// `tsconfig.json`.
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
let tsc = merge(
tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)),
tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir))
);
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 = 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;
});
// Copy all addons from ${srcDir}/ to ${outDir}/
let copyAddons = gulp.src([
`${srcDir}/addons/**/**`
]).pipe(gulp.dest(`${outDir}/addons`));
// Copy stylesheets from ${srcDir}/ to ${outDir}/
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
// Join all streams into a single array
let streams = [tsc].concat(addonStreams).concat([copyAddons, copyStylesheets]);
return merge.apply(this, streams);
gulp.task('watch-css', function() {
return gulp.watch(`${srcDir}/**/*.css`, ['css']);
});
/**
* Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with
* Browserify.
*/
gulp.task('browserify', ['tsc'], function() {
gulp.task('browserify', function() {
// Ensure that the build directory exists
fs.ensureDirSync(buildDir);
@@ -108,7 +74,7 @@ gulp.task('browserify', ['tsc'], function() {
return merge(bundleStream, copyStylesheets);
});
gulp.task('browserify-addons', ['tsc'], function() {
gulp.task('browserify-addons', function() {
const bundles = addons.map((addon) => {
const addonOptions = {
basedir: `${buildDir}/addons/${addon}`,
@@ -134,37 +100,22 @@ gulp.task('browserify-addons', ['tsc'], function() {
return merge(...bundles);
});
gulp.task('instrument-test', function () {
return gulp.src([`${outDir}/**/*.js`])
// Covering files
.pipe(istanbul())
// Force `require` to return covered files
.pipe(istanbul.hookRequire());
});
gulp.task('mocha', ['instrument-test'], function () {
return gulp.src([
`${outDir}/*test.js`,
`${outDir}/**/*test.js`,
`${outDir}/*integration.js`,
`${outDir}/**/*integration.js`
], {read: false})
gulp.task('mocha', function () {
return gulp.src(TEST_PATHS, {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
.once('error', () => process.exit(1));
});
/**
* Run single test file by file name(without file extension). Example of the command:
* gulp mocha-test --test InputHandler.test
* Run single test suite (file) by file name (without file extension). Example of the command:
* gulp mocha-suite --test InputHandler.test
*/
gulp.task('mocha-test', ['instrument-test'], function () {
gulp.task('mocha-suite', [], function () {
let testName = util.env.test;
util.log("Run test by Name: " + testName);
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
.pipe(mocha())
.once('error', () => process.exit(1))
.pipe(istanbul.writeReports());
.once('error', () => process.exit(1));
});
/**
@@ -192,16 +143,9 @@ 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
*/
gulp.task('coveralls', function () {
gulp.src('coverage/**/lcov.info')
.pipe(coveralls());
gulp.task('watch-demo', ['webpack'], () => {
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
});
gulp.task('build', ['sorcery', 'sorcery-addons']);
+17 -12
View File
@@ -1,19 +1,21 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.4.0",
"main": "lib/Terminal.js",
"version": "3.5.1",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"devDependencies": {
"@types/chai": "^3.4.34",
"@types/jsdom": "^11.0.1",
"@types/glob": "^5.0.35",
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "6.0.108",
"@types/text-encoding": "0.0.32",
"browserify": "^13.3.0",
"chai": "3.5.0",
"concurrently": "^3.5.1",
"coveralls": "^3.0.1",
"express": "4.13.4",
"express-ws": "2.0.0-rc.1",
"fs-extra": "^1.0.0",
@@ -21,22 +23,21 @@
"gulp": "3.9.1",
"gulp-cli": "^1.2.2",
"gulp-concat": "^2.6.1",
"gulp-coveralls": "^0.1.4",
"gulp-istanbul": "^1.1.1",
"gulp-mocha": "^3.0.1",
"gulp-sourcemaps": "1.9.1",
"gulp-typescript": "^3.1.3",
"gulp-util": "3.0.8",
"jsdoc": "3.4.3",
"jsdom": "^11.1.0",
"jsdom": "^11.11.0",
"merge-stream": "^1.0.1",
"node-pty": "^0.7.2",
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"nyc": "^11.8.0",
"sorcery": "^0.10.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "~2.7.1",
"typescript": "2.8.3",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
@@ -44,17 +45,21 @@
"zmodem.js": "^0.1.5"
},
"scripts": {
"start": "node demo/app",
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts'",
"test": "npm-run-all mocha lint",
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
"test-suite": "gulp mocha-suite --test",
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
"mocha": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc",
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
"build": "gulp build",
"prepublish": "npm run build",
"coveralls": "gulp coveralls",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"webpack": "gulp webpack",
"watch": "gulp watch"
"watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\""
}
}
+19 -21
View File
@@ -6,9 +6,9 @@
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
BOTTOM
}
export class AccessibilityManager implements IDisposable {
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
this.register(this._renderRowsDebouncer);
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
@@ -179,7 +177,7 @@ export class AccessibilityManager implements IDisposable {
this._refreshRowsDimensions();
}
public _createAccessibilityTreeNode(): HTMLElement {
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
+135 -1
View File
@@ -6,7 +6,7 @@
import { assert } from 'chai';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
const INIT_COLS = 80;
@@ -48,6 +48,63 @@ describe('Buffer', () => {
});
});
describe('getWrappedRangeForLine', () => {
describe('non-wrapped', () => {
it('should return a single row for the first row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 });
});
it('should return a single row for a middle row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 });
});
it('should return a single row for the last row', () => {
buffer.fillViewportRows();
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 });
});
});
describe('wrapped', () => {
it('should return a range for the first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
});
it('should return a range for a middle row wrapping upwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(12)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
});
it('should return a range for a middle row wrapping downwards', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(13)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
});
it('should return a range for a middle row wrapping both ways', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(11)).isWrapped = true;
(<any> buffer.lines.get(12)).isWrapped = true;
(<any> buffer.lines.get(13)).isWrapped = true;
(<any> buffer.lines.get(14)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
});
it('should return a range for the last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(23)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
});
it('should return a range for a row that wraps upward to first row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
});
it('should return a range for a row that wraps downward to last row', () => {
buffer.fillViewportRows();
(<any> buffer.lines.get(buffer.lines.length - 1)).isWrapped = true;
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
});
});
});
describe('resize', () => {
describe('column size is reduced', () => {
it('should not trim the data in the buffer', () => {
@@ -212,4 +269,81 @@ describe('Buffer', () => {
assert.equal(buffer.markers.length, 0);
});
});
describe ('translateBufferLineToString', () => {
it('should handle selecting a section of ascii text', () => {
buffer.lines.set(0, [
[ null, 'a', 1, 'a'.charCodeAt(0)],
[ null, 'b', 1, 'b'.charCodeAt(0)],
[ null, 'c', 1, 'c'.charCodeAt(0)],
[ null, 'd', 1, 'd'.charCodeAt(0)]
]);
const str = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str, 'ab');
});
it('should handle a cut-off double width character by including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, 35486 ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '語');
});
it('should handle a zero width character in the middle of the string by not including it', () => {
buffer.lines.set(0, [
[ null, '語', 2, '語'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str0, '語');
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str1, '語');
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str2, '語a');
});
it('should handle single width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 1, '😁'.charCodeAt(0) ],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁a');
});
it('should handle double width emojis', () => {
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null]
]);
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
assert.equal(str1, '😁');
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
assert.equal(str2, '😁');
buffer.lines.set(0, [
[ null, '😁', 2, '😁'.charCodeAt(0) ],
[ null, '', 0, null],
[ null, 'a', 1, 'a'.charCodeAt(0)]
]);
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
assert.equal(str3, '😁a');
});
});
});
+25 -11
View File
@@ -3,11 +3,12 @@
* @license MIT
*/
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
import { IMarker } from 'xterm';
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
export const CHAR_DATA_WIDTH_INDEX = 2;
@@ -116,7 +117,7 @@ export class Buffer implements IBuffer {
if (this.lines.length > 0) {
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this.lines.length; i++) {
while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
@@ -226,7 +227,7 @@ export class Buffer implements IBuffer {
if (startCol >= i) {
startIndex--;
}
if (endCol >= i) {
if (endCol > i) {
endIndex--;
}
} else {
@@ -259,6 +260,20 @@ export class Buffer implements IBuffer {
return lineString.substring(startIndex, endIndex);
}
public getWrappedRangeForLine(y: number): { first: number, last: number } {
let first = y;
let last = y;
// Scan upwards for wrapped lines
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
first--;
}
// Scan downwards for wrapped lines
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
last++;
}
return { first, last };
}
/**
* Setup the tab stops.
* @param i The index to start setting up tab stops from.
@@ -305,14 +320,14 @@ export class Buffer implements IBuffer {
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.register(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
@@ -323,11 +338,10 @@ export class Buffer implements IBuffer {
}
export class Marker extends EventEmitter implements IMarker {
private static NEXT_ID = 1;
private static _nextId = 1;
private _id: number = Marker.NEXT_ID++;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
@@ -342,8 +356,8 @@ export class Marker extends EventEmitter implements IMarker {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+2 -2
View File
@@ -121,7 +121,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
}
const control = opts.control | 0;
let table: number[] | Uint32Array = null;
function init_table(): number[] | Uint32Array {
function initTable(): number[] | Uint32Array {
// lookup table for BMP
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
@@ -161,7 +161,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
if (num < 127) {
return 1;
}
let t = table || init_table();
const t = table || initTable();
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
+33 -32
View File
@@ -5,13 +5,14 @@
import { assert } from 'chai';
import { CompositionHelper } from './CompositionHelper';
import { ITerminal } from './Types';
describe('CompositionHelper', () => {
let terminal;
let compositionHelper;
let compositionView;
let textarea;
let handledText;
let terminal: ITerminal;
let compositionHelper: CompositionHelper;
let compositionView: HTMLElement;
let textarea: HTMLTextAreaElement;
let handledText: string;
beforeEach(() => {
compositionView = {
@@ -27,14 +28,14 @@ describe('CompositionHelper', () => {
top: 0
},
textContent: ''
};
} as any;
textarea = {
value: '',
style: {
left: 0,
top: 0
}
};
} as any;
terminal = {
element: {
querySelector: () => {
@@ -54,7 +55,7 @@ describe('CompositionHelper', () => {
options: {
lineHeight: 1
}
};
} as any;
handledText = '';
compositionHelper = new CompositionHelper(textarea, compositionView, terminal);
});
@@ -63,7 +64,7 @@ describe('CompositionHelper', () => {
it('Should insert simple characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -71,7 +72,7 @@ describe('CompositionHelper', () => {
assert.equal(handledText, 'ㅇ');
// Second character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -87,13 +88,13 @@ describe('CompositionHelper', () => {
it('Should insert complex characters', (done) => {
// First character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -101,13 +102,13 @@ describe('CompositionHelper', () => {
assert.equal(handledText, '앙');
// Second character '앙'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = '앙ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '앙아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -127,19 +128,19 @@ describe('CompositionHelper', () => {
it('Should insert complex characters that change with following character', (done) => {
// First character '아'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아';
setTimeout(() => { // wait for any textarea updates
// Start second character '아' in first character
compositionHelper.compositionupdate({ data: '앙' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
textarea.value = '앙';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: '아' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
textarea.value = '아아';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -156,14 +157,14 @@ describe('CompositionHelper', () => {
it('Should insert multi-characters compositions', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'あ'
compositionHelper.compositionupdate({ data: 'だあ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だあ' });
textarea.value = 'だあ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -179,18 +180,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => {
// First character 'だ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'd' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
textarea.value = 'd';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'だ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
textarea.value = 'だ';
setTimeout(() => { // wait for any textarea updates
// Second character 'ー'
compositionHelper.compositionupdate({ data: 'だー' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だー' });
textarea.value = 'だー';
setTimeout(() => { // wait for any textarea updates
// Convert to katakana 'ダー'
compositionHelper.compositionupdate({ data: 'ダー' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ダー' });
textarea.value = 'ダー';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -207,18 +208,18 @@ describe('CompositionHelper', () => {
it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => {
// First character 'い'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'い' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'い' });
textarea.value = 'い';
setTimeout(() => { // wait for any textarea updates
// Second character 'ま'
compositionHelper.compositionupdate({ data: 'いm' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いm' });
textarea.value = 'いm';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionupdate({ data: 'いま' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いま' });
textarea.value = 'いま';
setTimeout(() => { // wait for any textarea updates
// Convert to kanji '今'
compositionHelper.compositionupdate({ data: '今' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: '今' });
textarea.value = '今';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
@@ -235,7 +236,7 @@ describe('CompositionHelper', () => {
it('Should insert non-composition characters input immediately after composition characters', (done) => {
// First character 'ㅇ'
compositionHelper.compositionstart();
compositionHelper.compositionupdate({ data: 'ㅇ' });
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
textarea.value = 'ㅇ';
setTimeout(() => { // wait for any textarea updates
compositionHelper.compositionend();
File diff suppressed because it is too large Load Diff
+565
View File
@@ -0,0 +1,565 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
import { Disposable } from './common/Lifecycle';
/**
* Returns an array filled with numbers between the low and high parameters (right exclusive).
* @param low The low number.
* @param high The high number.
*/
function r(low: number, high: number): number[] {
let c = high - low;
const arr = new Array(c);
while (c--) {
arr[c] = --high;
}
return arr;
}
/**
* Transition table for EscapeSequenceParser.
* NOTE: data in the underlying table is packed like this:
* currentState << 8 | characterCode --> action << 4 | nextState
*/
export class TransitionTable {
public table: Uint8Array | number[];
constructor(length: number) {
this.table = (typeof Uint8Array === 'undefined')
? new Array(length)
: new Uint8Array(length);
}
/**
* Add a transition to the transition table.
* @param code input character code
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
add(code: number, state: number, action: number | null, next: number | null): void {
this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next);
}
/**
* Add transitions for multiple input character codes.
* @param codes input character code array
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
addMany(codes: number[], state: number, action: number | null, next: number | null): void {
for (let i = 0; i < codes.length; i++) {
this.add(codes[i], state, action, next);
}
}
}
/**
* Default definitions for the VT500_TRANSITION_TABLE.
*/
const PRINTABLES = r(0x20, 0x7f);
const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19);
EXECUTABLES.concat(r(0x1c, 0x20));
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
/**
* VT500 compatible transition table.
* Taken from https://vt100.net/emu/dec_ansi_parser.
*/
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
const table: TransitionTable = new TransitionTable(4095);
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
let state: any;
// table with default transition [any] --> DEFAULT_TRANSITION
for (state in states) {
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small
for (let code = 0; code < 160; ++code) {
table.add(code, state, ParserAction.ERROR, ParserState.GROUND);
}
}
// printables
table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
// global anywhere rules
for (state in states) {
table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
}
// rules for executables and 7f
table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);
table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);
table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);
table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);
table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);
table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);
table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);
table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);
table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
// osc
table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);
table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
// sos/pm/apc does nothing
table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
// csi entries
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
// esc_intermediate
table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
// dcs entry
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
return table;
})();
/**
* Dummy DCS handler as default fallback.
*/
class DcsDummy implements IDcsHandler {
hook(collect: string, params: number[], flag: number): void { }
put(data: string, start: number, end: number): void { }
unhook(): void { }
}
/**
* EscapeSequenceParser.
* This class implements the ANSI/DEC compatible parser described by
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
* To implement custom ANSI compliant escape sequences it is not needed to
* alter this parser, instead consider registering a custom handler.
* For non ANSI compliant sequences change the transition table with
* the optional `transitions` contructor argument and
* reimplement the `parse` method.
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
// buffers over several parse calls
protected _osc: string;
protected _params: number[];
protected _collect: string;
// handler lookup containers
protected _printHandler: (data: string, start: number, end: number) => void;
protected _executeHandlers: any;
protected _csiHandlers: any;
protected _escHandlers: any;
protected _oscHandlers: any;
protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler | null;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
protected _printHandlerFb: (data: string, start: number, end: number) => void;
protected _executeHandlerFb: (code: number) => void;
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
protected _escHandlerFb: (collect: string, flag: number) => void;
protected _oscHandlerFb: (identifier: number, data: string) => void;
protected _dcsHandlerFb: IDcsHandler;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._collect = '';
// set default fallback handlers and handler lookup containers
this._printHandlerFb = (data, start, end): void => { };
this._executeHandlerFb = (code: number): void => { };
this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { };
this._escHandlerFb = (collect: string, flag: number): void => { };
this._oscHandlerFb = (identifier: number, data: string): void => { };
this._dcsHandlerFb = new DcsDummy();
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
this._printHandler = this._printHandlerFb;
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscHandlers = Object.create(null);
this._dcsHandlers = Object.create(null);
this._activeDcsHandler = null;
this._errorHandler = this._errorHandlerFb;
}
public dispose(): void {
this._printHandlerFb = null;
this._executeHandlerFb = null;
this._csiHandlerFb = null;
this._escHandlerFb = null;
this._oscHandlerFb = null;
this._dcsHandlerFb = null;
this._errorHandlerFb = null;
this._printHandler = null;
this._executeHandlers = null;
this._csiHandlers = null;
this._escHandlers = null;
this._oscHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
this._errorHandler = null;
}
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
this._printHandler = callback;
}
clearPrintHandler(): void {
this._printHandler = this._printHandlerFb;
}
setExecuteHandler(flag: string, callback: () => void): void {
this._executeHandlers[flag.charCodeAt(0)] = callback;
}
clearExecuteHandler(flag: string): void {
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
}
setExecuteHandlerFallback(callback: (code: number) => void): void {
this._executeHandlerFb = callback;
}
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = callback;
}
clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
}
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void {
this._csiHandlerFb = callback;
}
setEscHandler(collectAndFlag: string, callback: () => void): void {
this._escHandlers[collectAndFlag] = callback;
}
clearEscHandler(collectAndFlag: string): void {
if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag];
}
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void {
this._escHandlerFb = callback;
}
setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = callback;
}
clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
}
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
this._oscHandlerFb = callback;
}
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
this._dcsHandlers[collectAndFlag] = handler;
}
clearDcsHandler(collectAndFlag: string): void {
if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag];
}
setDcsHandlerFallback(handler: IDcsHandler): void {
this._dcsHandlerFb = handler;
}
setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
this._errorHandler = callback;
}
clearErrorHandler(): void {
this._errorHandler = this._errorHandlerFb;
}
reset(): void {
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._collect = '';
this._activeDcsHandler = null;
}
parse(data: string): void {
let code = 0;
let transition = 0;
let error = false;
let currentState = this.currentState;
let print = -1;
let dcs = -1;
let osc = this._osc;
let collect = this._collect;
let params = this._params;
const table: Uint8Array | number[] = this.TRANSITIONS.table;
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
let callback: Function | null = null;
// process input string
const l = data.length;
for (let i = 0; i < l; ++i) {
code = data.charCodeAt(i);
// shortcut for most chars (print action)
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
print = (~print) ? print : i;
do code = data.charCodeAt(++i);
while (i < l && code > 0x1f && code < 0x80);
i--;
continue;
}
// shortcut for CSI params
if (currentState === ParserState.CSI_PARAM && (code > 0x2f && code < 0x39)) {
params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
continue;
}
// normal transition & action lookup
transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION;
switch (transition >> 4) {
case ParserAction.PRINT:
print = (~print) ? print : i;
break;
case ParserAction.EXECUTE:
if (~print) {
this._printHandler(data, print, i);
print = -1;
}
callback = this._executeHandlers[code];
if (callback) callback();
else this._executeHandlerFb(code);
break;
case ParserAction.IGNORE:
// handle leftover print or dcs chars
if (~print) {
this._printHandler(data, print, i);
print = -1;
} else if (~dcs) {
dcsHandler.put(data, dcs, i);
dcs = -1;
}
break;
case ParserAction.ERROR:
// chars higher than 0x9f are handled by this action
// to keep the transition table small
if (code > 0x9f) {
switch (currentState) {
case ParserState.GROUND:
print = (~print) ? print : i;
break;
case ParserState.OSC_STRING:
osc += String.fromCharCode(code);
transition |= ParserState.OSC_STRING;
break;
case ParserState.CSI_IGNORE:
transition |= ParserState.CSI_IGNORE;
break;
case ParserState.DCS_IGNORE:
transition |= ParserState.DCS_IGNORE;
break;
case ParserState.DCS_PASSTHROUGH:
dcs = (~dcs) ? dcs : i;
transition |= ParserState.DCS_PASSTHROUGH;
break;
default:
error = true;
}
} else {
error = true;
}
// if we end up here a real error happened
if (error) {
const inject: IParsingState = this._errorHandler(
{
position: i,
code,
currentState,
print,
dcs,
osc,
collect,
params,
abort: false
});
if (inject.abort) return;
// TODO: inject return values
error = false;
}
break;
case ParserAction.CSI_DISPATCH:
callback = this._csiHandlers[code];
if (callback) callback(params, collect);
else this._csiHandlerFb(collect, params, code);
break;
case ParserAction.PARAM:
if (code === 0x3b) params.push(0);
else params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
break;
case ParserAction.COLLECT:
collect += String.fromCharCode(code);
break;
case ParserAction.ESC_DISPATCH:
callback = this._escHandlers[collect + String.fromCharCode(code)];
if (callback) callback(collect, code);
else this._escHandlerFb(collect, code);
break;
case ParserAction.CLEAR:
if (~print) {
this._printHandler(data, print, i);
print = -1;
}
osc = '';
params = [0];
collect = '';
dcs = -1;
break;
case ParserAction.DCS_HOOK:
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
dcsHandler.hook(collect, params, code);
break;
case ParserAction.DCS_PUT:
dcs = (~dcs) ? dcs : i;
break;
case ParserAction.DCS_UNHOOK:
if (dcsHandler) {
if (~dcs) dcsHandler.put(data, dcs, i);
dcsHandler.unhook();
dcsHandler = null;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
collect = '';
dcs = -1;
break;
case ParserAction.OSC_START:
if (~print) {
this._printHandler(data, print, i);
print = -1;
}
osc = '';
break;
case ParserAction.OSC_PUT:
osc += data.charAt(i);
break;
case ParserAction.OSC_END:
if (osc && code !== 0x18 && code !== 0x1a) {
// NOTE: OSC subparsing is not part of the original parser
// we do basic identifier parsing here to offer a jump table for OSC as well
const idx = osc.indexOf(';');
if (idx === -1) {
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
} else {
// Note: NaN is not handled here
// either catch it with the fallback handler
// or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1);
callback = this._oscHandlers[identifier];
if (callback) callback(content);
else this._oscHandlerFb(identifier, content);
}
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
collect = '';
dcs = -1;
break;
}
currentState = transition & 15;
}
// push leftover pushable buffers to terminal
if (currentState === ParserState.GROUND && ~print) {
this._printHandler(data, print, data.length);
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
dcsHandler.put(data, dcs, data.length);
}
// save non pushable buffers
this._osc = osc;
this._collect = collect;
this._params = params;
// save active dcs handler reference
this._activeDcsHandler = dcsHandler;
// save state
this.currentState = currentState;
}
}
+7 -3
View File
@@ -5,11 +5,13 @@
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter, IDisposable {
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
super();
// Restore the previous events if available, this will happen if the
// constructor is called multiple times on the same object (terminal reset).
this._events = this._events || {};
@@ -26,6 +28,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
this.on(type, handler);
return {
dispose: () => {
@@ -44,7 +47,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
return;
}
let obj = this._events[type];
const obj = this._events[type];
let i = obj.length;
while (i--) {
@@ -65,7 +68,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
if (!this._events[type]) {
return;
}
let obj = this._events[type];
const obj = this._events[type];
for (let i = 0; i < obj.length; i++) {
obj[i].apply(this, args);
}
@@ -76,6 +79,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
}
public dispose(): void {
super.dispose();
this._events = {};
}
}
+21 -16
View File
@@ -9,73 +9,78 @@ import { MockInputHandlingTerminal } from './utils/TestUtils.test';
describe('InputHandler', () => {
describe('save and restore cursor', () => {
let terminal = new MockInputHandlingTerminal();
const terminal = new MockInputHandlingTerminal();
terminal.buffer.x = 1;
terminal.buffer.y = 2;
let inputHandler = new InputHandler(terminal);
terminal.curAttr = 3;
const inputHandler = new InputHandler(terminal);
// Save cursor position
inputHandler.saveCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
// Change cursor position
terminal.buffer.x = 10;
terminal.buffer.y = 20;
terminal.curAttr = 30;
// Restore cursor position
inputHandler.restoreCursor([]);
assert.equal(terminal.buffer.x, 1);
assert.equal(terminal.buffer.y, 2);
assert.equal(terminal.curAttr, 3);
});
describe('setCursorStyle', () => {
it('should call Terminal.setOption with correct params', () => {
let terminal = new MockInputHandlingTerminal();
let inputHandler = new InputHandler(terminal);
const terminal = new MockInputHandlingTerminal();
const inputHandler = new InputHandler(terminal);
const collect = ' ';
inputHandler.setCursorStyle([0]);
inputHandler.setCursorStyle([0], collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([1]);
inputHandler.setCursorStyle([1], collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([2]);
inputHandler.setCursorStyle([2], collect);
assert.equal(terminal.options['cursorStyle'], 'block');
assert.equal(terminal.options['cursorBlink'], false);
terminal.options = {};
inputHandler.setCursorStyle([3]);
inputHandler.setCursorStyle([3], collect);
assert.equal(terminal.options['cursorStyle'], 'underline');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([4]);
inputHandler.setCursorStyle([4], collect);
assert.equal(terminal.options['cursorStyle'], 'underline');
assert.equal(terminal.options['cursorBlink'], false);
terminal.options = {};
inputHandler.setCursorStyle([5]);
inputHandler.setCursorStyle([5], collect);
assert.equal(terminal.options['cursorStyle'], 'bar');
assert.equal(terminal.options['cursorBlink'], true);
terminal.options = {};
inputHandler.setCursorStyle([6]);
inputHandler.setCursorStyle([6], collect);
assert.equal(terminal.options['cursorStyle'], 'bar');
assert.equal(terminal.options['cursorBlink'], false);
});
});
describe('setMode', () => {
it('should toggle Terminal.bracketedPasteMode', () => {
let terminal = new MockInputHandlingTerminal();
terminal.prefix = '?';
const terminal = new MockInputHandlingTerminal();
const collect = '?';
terminal.bracketedPasteMode = false;
let inputHandler = new InputHandler(terminal);
const inputHandler = new InputHandler(terminal);
// Set bracketed paste mode
inputHandler.setMode([2004]);
inputHandler.setMode([2004], collect);
assert.equal(terminal.bracketedPasteMode, true);
// Reset bracketed paste mode
inputHandler.resetMode([2004]);
inputHandler.resetMode([2004], collect);
assert.equal(terminal.bracketedPasteMode, false);
});
});
+600 -118
View File
@@ -4,12 +4,106 @@
* @license MIT
*/
import { CharData, IInputHandler, IInputHandlingTerminal } from './Types';
import { C0 } from './EscapeSequences';
import { DEFAULT_CHARSET } from './Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
import { CharData, IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { ICharset } from './core/Types';
import { Disposable } from './common/Lifecycle';
/**
* Map collect to glevel. Used in `selectCharset`.
*/
const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
/**
* DCS subparser implementations
*/
/**
* DCS + q Pt ST (xterm)
* Request Terminfo String
* not supported
*/
class RequestTerminfo implements IDcsHandler {
private _data: string;
constructor(private _terminal: any) { }
hook(collect: string, params: number[], flag: number): void {
this._data = '';
}
put(data: string, start: number, end: number): void {
this._data += data.substring(start, end);
}
unhook(): void {
// invalid: DCS 0 + r Pt ST
this._terminal.send(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
}
}
/**
* DCS $ q Pt ST
* DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)
* Request Status String (DECRQSS), VT420 and up.
* Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)
*/
class DECRQSS implements IDcsHandler {
private _data: string;
constructor(private _terminal: any) { }
hook(collect: string, params: number[], flag: number): void {
// reset data
this._data = '';
}
put(data: string, start: number, end: number): void {
this._data += data.substring(start, end);
}
unhook(): void {
switch (this._data) {
// valid: DCS 1 $ r Pt ST (xterm)
case '"q': // DECSCA
return this._terminal.send(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
case '"p': // DECSCL
return this._terminal.send(`${C0.ESC}P1$r61"p${C0.ESC}\\`);
case 'r': // DECSTBM
const pt = '' + (this._terminal.buffer.scrollTop + 1) +
';' + (this._terminal.buffer.scrollBottom + 1) + 'r';
return this._terminal.send(`${C0.ESC}P1$r${pt}${C0.ESC}\\`);
case 'm': // SGR
// TODO: report real settings instead of 0m
return this._terminal.send(`${C0.ESC}P1$r0m${C0.ESC}\\`);
case ' q': // DECSCUSR
const STYLES: {[key: string]: number} = {'block': 2, 'underline': 4, 'bar': 6};
let style = STYLES[this._terminal.getOption('cursorStyle')];
style -= this._terminal.getOption('cursorBlink');
return this._terminal.send(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
default:
// invalid: DCS 0 $ r Pt ST (xterm)
this._terminal.error('Unknown DCS $q %s', this._data);
this._terminal.send(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
}
}
}
/**
* DCS Ps; Ps| Pt ST
* DECUDK (https://vt100.net/docs/vt510-rm/DECUDK.html)
* not supported
*/
/**
* DCS + p Pt ST (xterm)
* Set Terminfo Data
* not supported
*/
/**
* The terminal's standard implementation of IInputHandler, this handles all
@@ -18,54 +112,291 @@ import { wcwidth } from './CharWidth';
* Refer to http://invisible-island.net/xterm/ctlseqs/ctlseqs.html to understand
* each function's header comment.
*/
export class InputHandler implements IInputHandler {
constructor(private _terminal: IInputHandlingTerminal) { }
export class InputHandler extends Disposable implements IInputHandler {
private _surrogateHigh: string;
public addChar(char: string, code: number): void {
if (char >= ' ') {
constructor(
private _terminal: any, // TODO: reestablish IInputHandlingTerminal here
private _parser: IEscapeSequenceParser = new EscapeSequenceParser())
{
super();
// make buffer local for faster access
const buffer = this._terminal.buffer;
this.register(this._parser);
this._surrogateHigh = '';
/**
* custom fallback handlers
*/
this._parser.setCsiHandlerFallback((collect: string, params: number[], flag: number) => {
this._terminal.error('Unknown CSI code: ', collect, params, String.fromCharCode(flag));
});
this._parser.setEscHandlerFallback((collect: string, flag: number) => {
this._terminal.error('Unknown ESC code: ', collect, String.fromCharCode(flag));
});
this._parser.setExecuteHandlerFallback((code: number) => {
this._terminal.error('Unknown EXECUTE code: ', code);
});
this._parser.setOscHandlerFallback((identifier: number, data: string) => {
this._terminal.error('Unknown OSC code: ', identifier, data);
});
/**
* print handler
*/
this._parser.setPrintHandler((data, start, end): void => this.print(data, start, end));
/**
* CSI handler
*/
this._parser.setCsiHandler('@', (params, collect) => this.insertChars(params));
this._parser.setCsiHandler('A', (params, collect) => this.cursorUp(params));
this._parser.setCsiHandler('B', (params, collect) => this.cursorDown(params));
this._parser.setCsiHandler('C', (params, collect) => this.cursorForward(params));
this._parser.setCsiHandler('D', (params, collect) => this.cursorBackward(params));
this._parser.setCsiHandler('E', (params, collect) => this.cursorNextLine(params));
this._parser.setCsiHandler('F', (params, collect) => this.cursorPrecedingLine(params));
this._parser.setCsiHandler('G', (params, collect) => this.cursorCharAbsolute(params));
this._parser.setCsiHandler('H', (params, collect) => this.cursorPosition(params));
this._parser.setCsiHandler('I', (params, collect) => this.cursorForwardTab(params));
this._parser.setCsiHandler('J', (params, collect) => this.eraseInDisplay(params));
this._parser.setCsiHandler('K', (params, collect) => this.eraseInLine(params));
this._parser.setCsiHandler('L', (params, collect) => this.insertLines(params));
this._parser.setCsiHandler('M', (params, collect) => this.deleteLines(params));
this._parser.setCsiHandler('P', (params, collect) => this.deleteChars(params));
this._parser.setCsiHandler('S', (params, collect) => this.scrollUp(params));
this._parser.setCsiHandler('T', (params, collect) => this.scrollDown(params, collect));
this._parser.setCsiHandler('X', (params, collect) => this.eraseChars(params));
this._parser.setCsiHandler('Z', (params, collect) => this.cursorBackwardTab(params));
this._parser.setCsiHandler('`', (params, collect) => this.charPosAbsolute(params));
this._parser.setCsiHandler('a', (params, collect) => this.hPositionRelative(params));
this._parser.setCsiHandler('b', (params, collect) => this.repeatPrecedingCharacter(params));
this._parser.setCsiHandler('c', (params, collect) => this.sendDeviceAttributes(params, collect));
this._parser.setCsiHandler('d', (params, collect) => this.linePosAbsolute(params));
this._parser.setCsiHandler('e', (params, collect) => this.vPositionRelative(params));
this._parser.setCsiHandler('f', (params, collect) => this.hVPosition(params));
this._parser.setCsiHandler('g', (params, collect) => this.tabClear(params));
this._parser.setCsiHandler('h', (params, collect) => this.setMode(params, collect));
this._parser.setCsiHandler('l', (params, collect) => this.resetMode(params, collect));
this._parser.setCsiHandler('m', (params, collect) => this.charAttributes(params));
this._parser.setCsiHandler('n', (params, collect) => this.deviceStatus(params, collect));
this._parser.setCsiHandler('p', (params, collect) => this.softReset(params, collect));
this._parser.setCsiHandler('q', (params, collect) => this.setCursorStyle(params, collect));
this._parser.setCsiHandler('r', (params, collect) => this.setScrollRegion(params, collect));
this._parser.setCsiHandler('s', (params, collect) => this.saveCursor(params));
this._parser.setCsiHandler('u', (params, collect) => this.restoreCursor(params));
/**
* execute handler
*/
this._parser.setExecuteHandler(C0.BEL, () => this.bell());
this._parser.setExecuteHandler(C0.LF, () => this.lineFeed());
this._parser.setExecuteHandler(C0.VT, () => this.lineFeed());
this._parser.setExecuteHandler(C0.FF, () => this.lineFeed());
this._parser.setExecuteHandler(C0.CR, () => this.carriageReturn());
this._parser.setExecuteHandler(C0.BS, () => this.backspace());
this._parser.setExecuteHandler(C0.HT, () => this.tab());
this._parser.setExecuteHandler(C0.SO, () => this.shiftOut());
this._parser.setExecuteHandler(C0.SI, () => this.shiftIn());
// FIXME: What do to with missing? Old code just added those to print.
// some C1 control codes - FIXME: should those be enabled by default?
this._parser.setExecuteHandler(C1.IND, () => this.index());
this._parser.setExecuteHandler(C1.NEL, () => this.nextLine());
this._parser.setExecuteHandler(C1.HTS, () => this.tabSet());
/**
* OSC handler
*/
// 0 - icon name + title
this._parser.setOscHandler(0, (data) => this.setTitle(data));
// 1 - icon name
// 2 - title
this._parser.setOscHandler(2, (data) => this.setTitle(data));
// 3 - set property X in the form "prop=value"
// 4 - Change Color Number
// 5 - Change Special Color Number
// 6 - Enable/disable Special Color Number c
// 7 - current directory? (not in xterm spec, see https://gitlab.com/gnachman/iterm2/issues/3939)
// 10 - Change VT100 text foreground color to Pt.
// 11 - Change VT100 text background color to Pt.
// 12 - Change text cursor color to Pt.
// 13 - Change mouse foreground color to Pt.
// 14 - Change mouse background color to Pt.
// 15 - Change Tektronix foreground color to Pt.
// 16 - Change Tektronix background color to Pt.
// 17 - Change highlight background color to Pt.
// 18 - Change Tektronix cursor color to Pt.
// 19 - Change highlight foreground color to Pt.
// 46 - Change Log File to Pt.
// 50 - Set Font to Pt.
// 51 - reserved for Emacs shell.
// 52 - Manipulate Selection Data.
// 104 ; c - Reset Color Number c.
// 105 ; c - Reset Special Color Number c.
// 106 ; c; f - Enable/disable Special Color Number c.
// 110 - Reset VT100 text foreground color.
// 111 - Reset VT100 text background color.
// 112 - Reset text cursor color.
// 113 - Reset mouse foreground color.
// 114 - Reset mouse background color.
// 115 - Reset Tektronix foreground color.
// 116 - Reset Tektronix background color.
// 117 - Reset highlight color.
// 118 - Reset Tektronix cursor color.
// 119 - Reset highlight foreground color.
/**
* ESC handlers
*/
this._parser.setEscHandler('7', () => this.saveCursor([]));
this._parser.setEscHandler('8', () => this.restoreCursor([]));
this._parser.setEscHandler('D', () => this.index());
this._parser.setEscHandler('E', () => this.nextLine());
this._parser.setEscHandler('H', () => this.tabSet());
this._parser.setEscHandler('M', () => this.reverseIndex());
this._parser.setEscHandler('=', () => this.keypadApplicationMode());
this._parser.setEscHandler('>', () => this.keypadNumericMode());
this._parser.setEscHandler('c', () => this.reset());
this._parser.setEscHandler('n', () => this.setgLevel(2));
this._parser.setEscHandler('o', () => this.setgLevel(3));
this._parser.setEscHandler('|', () => this.setgLevel(3));
this._parser.setEscHandler('}', () => this.setgLevel(2));
this._parser.setEscHandler('~', () => this.setgLevel(1));
this._parser.setEscHandler('%@', () => this.selectDefaultCharset());
this._parser.setEscHandler('%G', () => this.selectDefaultCharset());
for (const flag in CHARSETS) {
this._parser.setEscHandler('(' + flag, () => this.selectCharset('(' + flag));
this._parser.setEscHandler(')' + flag, () => this.selectCharset(')' + flag));
this._parser.setEscHandler('*' + flag, () => this.selectCharset('*' + flag));
this._parser.setEscHandler('+' + flag, () => this.selectCharset('+' + flag));
this._parser.setEscHandler('-' + flag, () => this.selectCharset('-' + flag));
this._parser.setEscHandler('.' + flag, () => this.selectCharset('.' + flag));
this._parser.setEscHandler('/' + flag, () => this.selectCharset('/' + flag)); // TODO: supported?
}
/**
* error handler
*/
this._parser.setErrorHandler((state) => {
this._terminal.error('Parsing error: ', state);
return state;
});
/**
* DCS handler
*/
this._parser.setDcsHandler('$q', new DECRQSS(this._terminal));
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
}
public dispose(): void {
super.dispose();
this._terminal = null;
}
public parse(data: string): void {
let buffer = this._terminal.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
if (this._terminal.debug) {
this._terminal.log('data: ' + data);
}
// apply leftover surrogate high from last write
if (this._surrogateHigh) {
data = this._surrogateHigh + data;
this._surrogateHigh = '';
}
this._parser.parse(data);
buffer = this._terminal.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._terminal.emit('cursormove');
}
}
public print(data: string, start: number, end: number): void {
let char: string;
let code: number;
let low: number;
let chWidth: number;
const buffer: IBuffer = this._terminal.buffer;
const charset: ICharset = this._terminal.charset;
const screenReaderMode: boolean = this._terminal.options.screenReaderMode;
const cols: number = this._terminal.cols;
const wraparoundMode: boolean = this._terminal.wraparoundMode;
const insertMode: boolean = this._terminal.insertMode;
const curAttr: number = this._terminal.curAttr;
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
this._terminal.updateRange(buffer.y);
for (let stringPosition = start; stringPosition < end; ++stringPosition) {
char = data.charAt(stringPosition);
code = data.charCodeAt(stringPosition);
// surrogate pair handling
if (0xD800 <= code && code <= 0xDBFF) {
// we got a surrogate high
// get surrogate low (next 2 bytes)
low = data.charCodeAt(stringPosition + 1);
if (isNaN(low)) {
// end of data stream, save surrogate high
this._surrogateHigh = char;
continue;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
char += data.charAt(stringPosition + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
// calculate print space
// expensive call, therefore we save width in line buffer
const chWidth = wcwidth(code);
chWidth = wcwidth(code);
if (this._terminal.charset && this._terminal.charset[char]) {
char = this._terminal.charset[char];
// get charset replacement character
if (charset) {
char = charset[char] || char;
code = char.charCodeAt(0);
}
if (this._terminal.options.screenReaderMode) {
if (screenReaderMode) {
this._terminal.emit('a11y.char', char);
}
let row = buffer.y + buffer.ybase;
// insert combining char in last cell
// insert combining char at last cursor position
// FIXME: needs handling after cursor jumps
// buffer.x should never be 0 for a combining char
// since they always follow a cell consuming char
// therefore we can test for buffer.x to avoid overflow left
if (!chWidth && buffer.x) {
// dont overflow left
if (buffer.lines.get(row)[buffer.x - 1]) {
if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
if (bufferRow[buffer.x - 1]) {
if (!bufferRow[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
if (buffer.lines.get(row)[buffer.x - 2]) {
buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0);
// it is save to step 2 cells back here
// since an empty cell is only set by fullwidth chars
if (bufferRow[buffer.x - 2]) {
bufferRow[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 2][CHAR_DATA_CODE_INDEX] = code;
}
} else {
buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0);
bufferRow[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
bufferRow[buffer.x - 1][CHAR_DATA_CODE_INDEX] = code;
}
this._terminal.updateRange(buffer.y);
}
return;
continue;
}
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (buffer.x + chWidth - 1 >= this._terminal.cols) {
if (buffer.x + chWidth - 1 >= cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
// automatically wraps to the beginning of the next line
if (wraparoundMode) {
buffer.x = 0;
buffer.y++;
if (buffer.y > buffer.scrollBottom) {
@@ -76,42 +407,47 @@ export class InputHandler implements IInputHandler {
// wrapped line
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
}
// row changed, get it again
bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
} else {
if (chWidth === 2) { // FIXME: check for xterm behavior
return;
if (chWidth === 2) {
// FIXME: check for xterm behavior
// What to do here? We got a wide char that does not fit into last cell
continue;
}
// FIXME: Do we have to set buffer.x to cols - 1, if not wrapping?
}
}
row = buffer.y + buffer.ybase;
// insert mode: move characters to right
if (this._terminal.insertMode) {
// To achieve insert, we remove cells from the right
// and insert empty ones at cursor position
if (insertMode) {
// do this twice for a fullwidth char
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 = buffer.lines.get(buffer.y + buffer.ybase).pop();
// remove last cell
// if it's width is 0, we have to adjust the second last cell as well
const removed = bufferRow.pop();
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
&& buffer.lines.get(row)[this._terminal.cols - 2]
&& buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
&& bufferRow[this._terminal.cols - 2]
&& bufferRow[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
bufferRow[this._terminal.cols - 2] = [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ];
}
// insert empty cell at cursor
buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
bufferRow.splice(buffer.x, 0, [curAttr, ' ', 1, 32 /* ' '.charCodeAt(0) */ ]);
}
}
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
buffer.x++;
this._terminal.updateRange(buffer.y);
// write current char to buffer and advance cursor
bufferRow[buffer.x++] = [curAttr, char, chWidth, code];
// fullwidth char - set next cell width to zero and advance cursor
// fullwidth char - also set next cell to placeholder stub and advance cursor
if (chWidth === 2) {
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined];
buffer.x++;
bufferRow[buffer.x++] = [curAttr, '', 0, undefined];
}
}
this._terminal.updateRange(buffer.y);
}
/**
@@ -461,10 +797,10 @@ export class InputHandler implements IInputHandler {
// make buffer local for faster access
const buffer = this._terminal.buffer;
let row: number = buffer.y + buffer.ybase;
const row: number = buffer.y + buffer.ybase;
let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom;
let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1;
const scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom;
const scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1;
while (param--) {
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
@@ -551,19 +887,21 @@ export class InputHandler implements IInputHandler {
/**
* CSI Ps T Scroll down Ps lines (default = 1) (SD).
*/
public scrollDown(params: number[]): void {
let param = params[0] || 1;
public scrollDown(params: number[], collect?: string): void {
if (params.length < 2 && !collect) {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
// this.maxRange();
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -622,7 +960,7 @@ export class InputHandler implements IInputHandler {
* [columns] (default = [row,col+1]) (HPR)
* reuse CSI Ps C ?
*/
public HPositionRelative(params: number[]): void {
public hPositionRelative(params: number[]): void {
let param = params[0];
if (param < 1) {
param = 1;
@@ -643,7 +981,7 @@ export class InputHandler implements IInputHandler {
const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y);
const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
const ch = line[buffer.x - 1] || [DEFAULT_ATTR, ' ', 1, 32];
while (param--) {
line[buffer.x++] = ch;
@@ -687,18 +1025,18 @@ export class InputHandler implements IInputHandler {
* xterm/charproc.c - line 2012, for more information.
* vim responds with ^[[?0c or ^[[?1c after the terminal's response (?)
*/
public sendDeviceAttributes(params: number[]): void {
public sendDeviceAttributes(params: number[], collect?: string): void {
if (params[0] > 0) {
return;
}
if (!this._terminal.prefix) {
if (!collect) {
if (this._terminal.is('xterm') || this._terminal.is('rxvt-unicode') || this._terminal.is('screen')) {
this._terminal.send(C0.ESC + '[?1;2c');
} else if (this._terminal.is('linux')) {
this._terminal.send(C0.ESC + '[?6c');
}
} else if (this._terminal.prefix === '>') {
} else if (collect === '>') {
// xterm and urxvt
// seem to spit this
// out around ~370 times (?).
@@ -736,7 +1074,7 @@ export class InputHandler implements IInputHandler {
* [rows] (default = [row+1,column])
* reuse CSI Ps B ?
*/
public VPositionRelative(params: number[]): void {
public vPositionRelative(params: number[]): void {
let param = params[0];
if (param < 1) {
param = 1;
@@ -756,7 +1094,7 @@ export class InputHandler implements IInputHandler {
* Horizontal and Vertical Position [row;column] (default =
* [1,1]) (HVP).
*/
public HVPosition(params: number[]): void {
public hVPosition(params: number[]): void {
if (params[0] < 1) params[0] = 1;
if (params[1] < 1) params[1] = 1;
@@ -780,7 +1118,7 @@ export class InputHandler implements IInputHandler {
* http://vt100.net/annarbor/aaa-ug/section6.html
*/
public tabClear(params: number[]): void {
let param = params[0];
const param = params[0];
if (param <= 0) {
delete this._terminal.buffer.tabs[this._terminal.buffer.x];
} else if (param === 3) {
@@ -874,7 +1212,7 @@ export class InputHandler implements IInputHandler {
* Modes:
* http: *vt100.net/docs/vt220-rm/chapter4.html
*/
public setMode(params: number[]): void {
public setMode(params: number[], collect?: string): void {
if (params.length > 1) {
for (let i = 0; i < params.length; i++) {
this.setMode([params[i]]);
@@ -883,7 +1221,7 @@ export class InputHandler implements IInputHandler {
return;
}
if (!this._terminal.prefix) {
if (!collect) {
switch (params[0]) {
case 4:
this._terminal.insertMode = true;
@@ -892,7 +1230,7 @@ export class InputHandler implements IInputHandler {
// this._t.convertEol = true;
break;
}
} else if (this._terminal.prefix === '?') {
} else if (collect === '?') {
switch (params[0]) {
case 1:
this._terminal.applicationCursor = true;
@@ -1068,7 +1406,7 @@ export class InputHandler implements IInputHandler {
* Ps = 1 0 6 1 -> Reset keyboard emulation to Sun/PC style.
* Ps = 2 0 0 4 -> Reset bracketed paste mode.
*/
public resetMode(params: number[]): void {
public resetMode(params: number[], collect?: string): void {
if (params.length > 1) {
for (let i = 0; i < params.length; i++) {
this.resetMode([params[i]]);
@@ -1077,7 +1415,7 @@ export class InputHandler implements IInputHandler {
return;
}
if (!this._terminal.prefix) {
if (!collect) {
switch (params[0]) {
case 4:
this._terminal.insertMode = false;
@@ -1086,7 +1424,7 @@ export class InputHandler implements IInputHandler {
// this._t.convertEol = false;
break;
}
} else if (this._terminal.prefix === '?') {
} else if (collect === '?') {
switch (params[0]) {
case 1:
this._terminal.applicationCursor = false;
@@ -1226,7 +1564,7 @@ export class InputHandler implements IInputHandler {
public charAttributes(params: number[]): void {
// Optimize a single SGR0.
if (params.length === 1 && params[0] === 0) {
this._terminal.curAttr = this._terminal.defAttr;
this._terminal.curAttr = DEFAULT_ATTR;
return;
}
@@ -1254,9 +1592,9 @@ export class InputHandler implements IInputHandler {
bg = p - 100;
} else if (p === 0) {
// default
flags = this._terminal.defAttr >> 18;
fg = (this._terminal.defAttr >> 9) & 0x1ff;
bg = this._terminal.defAttr & 0x1ff;
flags = DEFAULT_ATTR >> 18;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
// flags = 0;
// fg = 0x1ff;
// bg = 0x1ff;
@@ -1300,10 +1638,10 @@ export class InputHandler implements IInputHandler {
flags &= ~FLAGS.INVISIBLE;
} else if (p === 39) {
// reset fg
fg = (this._terminal.defAttr >> 9) & 0x1ff;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
} else if (p === 49) {
// reset bg
bg = this._terminal.defAttr & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
} else if (p === 38) {
// fg color 256
if (params[i + 1] === 2) {
@@ -1336,8 +1674,8 @@ export class InputHandler implements IInputHandler {
}
} else if (p === 100) {
// reset fg/bg
fg = (this._terminal.defAttr >> 9) & 0x1ff;
bg = this._terminal.defAttr & 0x1ff;
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
bg = DEFAULT_ATTR & 0x1ff;
} else {
this._terminal.error('Unknown SGR attribute: %d.', p);
}
@@ -1369,8 +1707,8 @@ export class InputHandler implements IInputHandler {
* CSI ? 5 3 n Locator available, if compiled-in, or
* CSI ? 5 0 n No Locator, if not.
*/
public deviceStatus(params: number[]): void {
if (!this._terminal.prefix) {
public deviceStatus(params: number[], collect?: string): void {
if (!collect) {
switch (params[0]) {
case 5:
// status report
@@ -1385,7 +1723,7 @@ export class InputHandler implements IInputHandler {
+ 'R');
break;
}
} else if (this._terminal.prefix === '?') {
} else if (collect === '?') {
// modern xterm doesnt seem to
// respond to any of these except ?6, 6, and 5
switch (params[0]) {
@@ -1421,21 +1759,23 @@ export class InputHandler implements IInputHandler {
* CSI ! p Soft terminal reset (DECSTR).
* http://vt100.net/docs/vt220-rm/table4-10.html
*/
public softReset(params: number[]): void {
this._terminal.cursorHidden = false;
this._terminal.insertMode = false;
this._terminal.originMode = false;
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this._terminal.applicationKeypad = false; // ?
this._terminal.viewport.syncScrollArea();
this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
this._terminal.curAttr = this._terminal.defAttr;
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
this._terminal.charset = null;
this._terminal.glevel = 0; // ??
this._terminal.charsets = [null]; // ??
public softReset(params: number[], collect?: string): void {
if (collect === '!') {
this._terminal.cursorHidden = false;
this._terminal.insertMode = false;
this._terminal.originMode = false;
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
this._terminal.applicationKeypad = false; // ?
this._terminal.viewport.syncScrollArea();
this._terminal.applicationCursor = false;
this._terminal.buffer.scrollTop = 0;
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
this._terminal.curAttr = DEFAULT_ATTR;
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
this._terminal.charset = null;
this._terminal.glevel = 0; // ??
this._terminal.charsets = [null]; // ??
}
}
/**
@@ -1448,24 +1788,26 @@ export class InputHandler implements IInputHandler {
* Ps = 5 -> blinking bar (xterm).
* Ps = 6 -> steady bar (xterm).
*/
public setCursorStyle(params?: number[]): void {
const param = params[0] < 1 ? 1 : params[0];
switch (param) {
case 1:
case 2:
this._terminal.setOption('cursorStyle', 'block');
break;
case 3:
case 4:
this._terminal.setOption('cursorStyle', 'underline');
break;
case 5:
case 6:
this._terminal.setOption('cursorStyle', 'bar');
break;
public setCursorStyle(params?: number[], collect?: string): void {
if (collect === ' ') {
const param = params[0] < 1 ? 1 : params[0];
switch (param) {
case 1:
case 2:
this._terminal.setOption('cursorStyle', 'block');
break;
case 3:
case 4:
this._terminal.setOption('cursorStyle', 'underline');
break;
case 5:
case 6:
this._terminal.setOption('cursorStyle', 'bar');
break;
}
const isBlinking = param % 2 === 1;
this._terminal.setOption('cursorBlink', isBlinking);
}
const isBlinking = param % 2 === 1;
this._terminal.setOption('cursorBlink', isBlinking);
}
/**
@@ -1474,8 +1816,8 @@ export class InputHandler implements IInputHandler {
* dow) (DECSTBM).
* CSI ? Pm r
*/
public setScrollRegion(params: number[]): void {
if (this._terminal.prefix) return;
public setScrollRegion(params: number[], collect?: string): void {
if (collect) return;
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
this._terminal.buffer.x = 0;
@@ -1485,20 +1827,160 @@ export class InputHandler implements IInputHandler {
/**
* CSI s
* ESC 7
* Save cursor (ANSI.SYS).
*/
public saveCursor(params: number[]): void {
this._terminal.buffer.savedX = this._terminal.buffer.x;
this._terminal.buffer.savedY = this._terminal.buffer.y;
this._terminal.savedCurAttr = this._terminal.curAttr;
}
/**
* CSI u
* ESC 8
* Restore cursor (ANSI.SYS).
*/
public restoreCursor(params: number[]): void {
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
this._terminal.curAttr = this._terminal.savedCurAttr || DEFAULT_ATTR;
}
/**
* OSC 0; <data> ST (set icon name + window title)
* OSC 2; <data> ST (set window title)
* Proxy to set window title. Icon name is not supported.
*/
public setTitle(data: string): void {
this._terminal.handleTitle(data);
}
/**
* ESC E
* C1.NEL
* DEC mnemonic: NEL (https://vt100.net/docs/vt510-rm/NEL)
* Moves cursor to first position on next line.
*/
public nextLine(): void {
this._terminal.buffer.x = 0;
this.index();
}
/**
* ESC =
* DEC mnemonic: DECKPAM (https://vt100.net/docs/vt510-rm/DECKPAM.html)
* Enables the numeric keypad to send application sequences to the host.
*/
public keypadApplicationMode(): void {
this._terminal.log('Serial port requested application keypad.');
this._terminal.applicationKeypad = true;
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
}
/**
* ESC >
* DEC mnemonic: DECKPNM (https://vt100.net/docs/vt510-rm/DECKPNM.html)
* Enables the keypad to send numeric characters to the host.
*/
public keypadNumericMode(): void {
this._terminal.log('Switching back to normal keypad.');
this._terminal.applicationKeypad = false;
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
}
/**
* ESC % @
* ESC % G
* Select default character set. UTF-8 is not supported (string are unicode anyways)
* therefore ESC % G does the same.
*/
public selectDefaultCharset(): void {
this._terminal.setgLevel(0);
this._terminal.setgCharset(0, DEFAULT_CHARSET); // US (default)
}
/**
* ESC ( C
* Designate G0 Character Set, VT100, ISO 2022.
* ESC ) C
* Designate G1 Character Set (ISO 2022, VT100).
* ESC * C
* Designate G2 Character Set (ISO 2022, VT220).
* ESC + C
* Designate G3 Character Set (ISO 2022, VT220).
* ESC - C
* Designate G1 Character Set (VT300).
* ESC . C
* Designate G2 Character Set (VT300).
* ESC / C
* Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?
*/
public selectCharset(collectAndFlag: string): void {
if (collectAndFlag.length !== 2) return this.selectDefaultCharset();
if (collectAndFlag[0] === '/') return; // TODO: Is this supported?
this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET);
}
/**
* ESC D
* C1.IND
* DEC mnemonic: IND (https://vt100.net/docs/vt510-rm/IND.html)
* Moves the cursor down one line in the same column.
*/
public index(): void {
this._terminal.index(); // TODO: save to move from terminal?
}
/**
* ESC H
* C1.HTS
* DEC mnemonic: HTS (https://vt100.net/docs/vt510-rm/HTS.html)
* Sets a horizontal tab stop at the column position indicated by
* the value of the active column when the terminal receives an HTS.
*/
public tabSet(): void {
this._terminal.tabSet(); // TODO: save to move from terminal?
}
/**
* ESC M
* C1.RI
* DEC mnemonic: HTS
* Moves the cursor up one line in the same column. If the cursor is at the top margin,
* the page scrolls down.
*/
public reverseIndex(): void {
this._terminal.reverseIndex(); // TODO: save to move from terminal?
}
/**
* ESC c
* DEC mnemonic: RIS (https://vt100.net/docs/vt510-rm/RIS.html)
* Reset to initial state.
*/
public reset(): void {
this._parser.reset();
this._terminal.reset(); // TODO: save to move from terminal?
}
/**
* ESC n
* ESC o
* ESC |
* ESC }
* ESC ~
* DEC mnemonic: LS (https://vt100.net/docs/vt510-rm/LS.html)
* When you use a locking shift, the character set remains in GL or GR until
* you use another locking shift. (partly supported)
*/
public setgLevel(level: number): void {
this._terminal.setgLevel(level); // TODO: save to move from terminal?
}
}
+8 -6
View File
@@ -4,16 +4,16 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(_terminal: ITerminal) {
super(_terminal);
Linkifier.TIME_BEFORE_LINKIFY = 0;
constructor(terminal: ITerminal) {
super(terminal);
(<any>Linkifier).TIME_BEFORE_LINKIFY = 0;
}
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
@@ -21,6 +21,8 @@ class TestLinkifier extends Linkifier {
}
class TestMouseZoneManager implements IMouseZoneManager {
dispose(): void {
}
public clears: number = 0;
public zones: IMouseZone[] = [];
add(zone: IMouseZone): void {
@@ -47,7 +49,7 @@ describe('Linkifier', () => {
});
function stringToRow(text: string): LineData {
let result: LineData = [];
const result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
+5 -5
View File
@@ -3,9 +3,9 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
/**
@@ -17,7 +17,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* the costly operation of searching every row multiple times, potentially a
* huge amount of times.
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected static readonly TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: ILinkMatcher[] = [];
@@ -199,11 +199,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Find the first match
let match = text.match(matcher.regex);
const match = text.match(matcher.regex);
if (!match || match.length === 0) {
return;
}
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
// Get index, match.index is for the outer match which includes negated chars
const index = text.indexOf(uri);
-673
View File
@@ -1,673 +0,0 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
import { C0 } from './EscapeSequences';
import { IInputHandler, IInputHandlingTerminal } from './Types';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
normalStateHandler[C0.BEL] = (parser, handler) => handler.bell();
normalStateHandler[C0.LF] = (parser, handler) => handler.lineFeed();
normalStateHandler[C0.VT] = normalStateHandler[C0.LF];
normalStateHandler[C0.FF] = normalStateHandler[C0.LF];
normalStateHandler[C0.CR] = (parser, handler) => handler.carriageReturn();
normalStateHandler[C0.BS] = (parser, handler) => handler.backspace();
normalStateHandler[C0.HT] = (parser, handler) => handler.tab();
normalStateHandler[C0.SO] = (parser, handler) => handler.shiftOut();
normalStateHandler[C0.SI] = (parser, handler) => handler.shiftIn();
normalStateHandler[C0.ESC] = (parser, handler) => parser.setState(ParserState.ESCAPED);
// TODO: Remove terminal when parser owns params and currentParam
const escapedStateHandler: {[key: string]: (parser: Parser, terminal: any) => void} = {};
escapedStateHandler['['] = (parser, terminal) => {
// ESC [ Control Sequence Introducer (CSI is 0x9b)
terminal.params = [];
terminal.currentParam = 0;
parser.setState(ParserState.CSI_PARAM);
};
escapedStateHandler[']'] = (parser, terminal) => {
// ESC ] Operating System Command (OSC is 0x9d)
terminal.params = [];
terminal.currentParam = 0;
parser.setState(ParserState.OSC);
};
escapedStateHandler['P'] = (parser, terminal) => {
// ESC P Device Control String (DCS is 0x90)
terminal.params = [];
terminal.currentParam = 0;
parser.setState(ParserState.DCS);
};
escapedStateHandler['_'] = (parser, terminal) => {
// ESC _ Application Program Command ( APC is 0x9f).
parser.setState(ParserState.IGNORE);
};
escapedStateHandler['^'] = (parser, terminal) => {
// ESC ^ Privacy Message ( PM is 0x9e).
parser.setState(ParserState.IGNORE);
};
escapedStateHandler['c'] = (parser, terminal) => {
// ESC c Full Reset (RIS).
terminal.reset();
};
escapedStateHandler['E'] = (parser, terminal) => {
// ESC E Next Line ( NEL is 0x85).
terminal.buffer.x = 0;
terminal.index();
parser.setState(ParserState.NORMAL);
};
escapedStateHandler['D'] = (parser, terminal) => {
// ESC D Index ( IND is 0x84).
terminal.index();
parser.setState(ParserState.NORMAL);
};
escapedStateHandler['M'] = (parser, terminal) => {
// ESC M Reverse Index ( RI is 0x8d).
terminal.reverseIndex();
parser.setState(ParserState.NORMAL);
};
escapedStateHandler['%'] = (parser, terminal) => {
// ESC % Select default/utf-8 character set.
// @ = default, G = utf-8
terminal.setgLevel(0);
terminal.setgCharset(0, DEFAULT_CHARSET); // US (default)
parser.setState(ParserState.NORMAL);
parser.skipNextChar();
};
escapedStateHandler[C0.CAN] = (parser) => parser.setState(ParserState.NORMAL);
const csiParamStateHandler: {[key: string]: (parser: Parser) => void} = {};
csiParamStateHandler['?'] = (parser) => parser.setPrefix('?');
csiParamStateHandler['>'] = (parser) => parser.setPrefix('>');
csiParamStateHandler['!'] = (parser) => parser.setPrefix('!');
csiParamStateHandler['0'] = (parser) => parser.setParam(parser.getParam() * 10);
csiParamStateHandler['1'] = (parser) => parser.setParam(parser.getParam() * 10 + 1);
csiParamStateHandler['2'] = (parser) => parser.setParam(parser.getParam() * 10 + 2);
csiParamStateHandler['3'] = (parser) => parser.setParam(parser.getParam() * 10 + 3);
csiParamStateHandler['4'] = (parser) => parser.setParam(parser.getParam() * 10 + 4);
csiParamStateHandler['5'] = (parser) => parser.setParam(parser.getParam() * 10 + 5);
csiParamStateHandler['6'] = (parser) => parser.setParam(parser.getParam() * 10 + 6);
csiParamStateHandler['7'] = (parser) => parser.setParam(parser.getParam() * 10 + 7);
csiParamStateHandler['8'] = (parser) => parser.setParam(parser.getParam() * 10 + 8);
csiParamStateHandler['9'] = (parser) => parser.setParam(parser.getParam() * 10 + 9);
csiParamStateHandler['$'] = (parser) => parser.setPostfix('$');
csiParamStateHandler['"'] = (parser) => parser.setPostfix('"');
csiParamStateHandler[' '] = (parser) => parser.setPostfix(' ');
csiParamStateHandler['\''] = (parser) => parser.setPostfix('\'');
csiParamStateHandler[';'] = (parser) => parser.finalizeParam();
csiParamStateHandler[C0.CAN] = (parser) => parser.setState(ParserState.NORMAL);
const csiStateHandler: {[key: string]: (handler: IInputHandler, params: number[], prefix: string, postfix: string, parser: Parser) => void} = {};
csiStateHandler['@'] = (handler, params, prefix) => handler.insertChars(params);
csiStateHandler['A'] = (handler, params, prefix) => handler.cursorUp(params);
csiStateHandler['B'] = (handler, params, prefix) => handler.cursorDown(params);
csiStateHandler['C'] = (handler, params, prefix) => handler.cursorForward(params);
csiStateHandler['D'] = (handler, params, prefix) => handler.cursorBackward(params);
csiStateHandler['E'] = (handler, params, prefix) => handler.cursorNextLine(params);
csiStateHandler['F'] = (handler, params, prefix) => handler.cursorPrecedingLine(params);
csiStateHandler['G'] = (handler, params, prefix) => handler.cursorCharAbsolute(params);
csiStateHandler['H'] = (handler, params, prefix) => handler.cursorPosition(params);
csiStateHandler['I'] = (handler, params, prefix) => handler.cursorForwardTab(params);
csiStateHandler['J'] = (handler, params, prefix) => handler.eraseInDisplay(params);
csiStateHandler['K'] = (handler, params, prefix) => handler.eraseInLine(params);
csiStateHandler['L'] = (handler, params, prefix) => handler.insertLines(params);
csiStateHandler['M'] = (handler, params, prefix) => handler.deleteLines(params);
csiStateHandler['P'] = (handler, params, prefix) => handler.deleteChars(params);
csiStateHandler['S'] = (handler, params, prefix) => handler.scrollUp(params);
csiStateHandler['T'] = (handler, params, prefix) => {
if (params.length < 2 && !prefix) {
handler.scrollDown(params);
}
};
csiStateHandler['X'] = (handler, params, prefix) => handler.eraseChars(params);
csiStateHandler['Z'] = (handler, params, prefix) => handler.cursorBackwardTab(params);
csiStateHandler['`'] = (handler, params, prefix) => handler.charPosAbsolute(params);
csiStateHandler['a'] = (handler, params, prefix) => handler.HPositionRelative(params);
csiStateHandler['b'] = (handler, params, prefix) => handler.repeatPrecedingCharacter(params);
csiStateHandler['c'] = (handler, params, prefix) => handler.sendDeviceAttributes(params);
csiStateHandler['d'] = (handler, params, prefix) => handler.linePosAbsolute(params);
csiStateHandler['e'] = (handler, params, prefix) => handler.VPositionRelative(params);
csiStateHandler['f'] = (handler, params, prefix) => handler.HVPosition(params);
csiStateHandler['g'] = (handler, params, prefix) => handler.tabClear(params);
csiStateHandler['h'] = (handler, params, prefix) => handler.setMode(params);
csiStateHandler['l'] = (handler, params, prefix) => handler.resetMode(params);
csiStateHandler['m'] = (handler, params, prefix) => handler.charAttributes(params);
csiStateHandler['n'] = (handler, params, prefix) => handler.deviceStatus(params);
csiStateHandler['p'] = (handler, params, prefix) => {
switch (prefix) {
case '!': handler.softReset(params); break;
}
};
csiStateHandler['q'] = (handler, params, prefix, postfix) => {
if (postfix === ' ') {
handler.setCursorStyle(params);
}
};
csiStateHandler['r'] = (handler, params) => handler.setScrollRegion(params);
csiStateHandler['s'] = (handler, params) => handler.saveCursor(params);
csiStateHandler['u'] = (handler, params) => handler.restoreCursor(params);
csiStateHandler[C0.CAN] = (handler, params, prefix, postfix, parser) => parser.setState(ParserState.NORMAL);
export const enum ParserState {
NORMAL = 0,
ESCAPED = 1,
CSI_PARAM = 2,
CSI = 3,
OSC = 4,
CHARSET = 5,
DCS = 6,
IGNORE = 7
}
/**
* The terminal's parser, all input into the terminal goes through the parser
* which parses and defers the actual input handling the the IInputHandler
* specified in the constructor.
*/
export class Parser {
private _state: ParserState;
private _position: number;
// TODO: Remove terminal when handler can do everything
constructor(
private _inputHandler: IInputHandler,
private _terminal: any
) {
this._state = ParserState.NORMAL;
}
/**
* Parse and handle data.
*
* @param data The data to parse.
*/
public parse(data: string): ParserState {
const l = data.length;
let cs;
let ch;
let code;
let low;
const cursorStartX = this._terminal.buffer.x;
const cursorStartY = this._terminal.buffer.y;
if (this._terminal.debug) {
this._terminal.log('data: ' + data);
}
this._position = 0;
// apply leftover surrogate high from last write
if (this._terminal.surrogate_high) {
data = this._terminal.surrogate_high + data;
this._terminal.surrogate_high = '';
}
for (; this._position < l; this._position++) {
ch = data[this._position];
// FIXME: higher chars than 0xa0 are not allowed in escape sequences
// --> maybe move to default
code = data.charCodeAt(this._position);
if (0xD800 <= code && code <= 0xDBFF) {
// we got a surrogate high
// get surrogate low (next 2 bytes)
low = data.charCodeAt(this._position + 1);
if (isNaN(low)) {
// end of data stream, save surrogate high
this._terminal.surrogate_high = ch;
continue;
}
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
ch += data.charAt(this._position + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
switch (this._state) {
case ParserState.NORMAL:
if (ch in normalStateHandler) {
normalStateHandler[ch](this, this._inputHandler);
} else {
this._inputHandler.addChar(ch, code);
}
break;
case ParserState.ESCAPED:
if (ch in escapedStateHandler) {
escapedStateHandler[ch](this, this._terminal);
// Skip switch as it was just handled
break;
}
switch (ch) {
// ESC (,),*,+,-,. Designate G0-G2 Character Set.
case '(': // <-- this seems to get all the attention
case ')':
case '*':
case '+':
case '-':
case '.':
switch (ch) {
case '(':
this._terminal.gcharset = 0;
break;
case ')':
this._terminal.gcharset = 1;
break;
case '*':
this._terminal.gcharset = 2;
break;
case '+':
this._terminal.gcharset = 3;
break;
case '-':
this._terminal.gcharset = 1;
break;
case '.':
this._terminal.gcharset = 2;
break;
}
this._state = ParserState.CHARSET;
break;
// Designate G3 Character Set (VT300).
// A = ISO Latin-1 Supplemental.
// Not implemented.
case '/':
this._terminal.gcharset = 3;
this._state = ParserState.CHARSET;
this._position--;
break;
// ESC N
// 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).
case '7':
this._inputHandler.saveCursor();
this._state = ParserState.NORMAL;
break;
// ESC 8 Restore Cursor (DECRC).
case '8':
this._inputHandler.restoreCursor();
this._state = ParserState.NORMAL;
break;
// ESC # 3 DEC line height/width
case '#':
this._state = ParserState.NORMAL;
this._position++;
break;
// ESC H Tab Set (HTS is 0x88).
case 'H':
(<IInputHandlingTerminal>this._terminal).tabSet();
this._state = ParserState.NORMAL;
break;
// ESC = Application Keypad (DECKPAM).
case '=':
this._terminal.log('Serial port requested application keypad.');
this._terminal.applicationKeypad = true;
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._state = ParserState.NORMAL;
break;
// ESC > Normal Keypad (DECKPNM).
case '>':
this._terminal.log('Switching back to normal keypad.');
this._terminal.applicationKeypad = false;
if (this._terminal.viewport) {
this._terminal.viewport.syncScrollArea();
}
this._state = ParserState.NORMAL;
break;
default:
this._state = ParserState.NORMAL;
this._terminal.error('Unknown ESC control: %s.', ch);
break;
}
break;
case ParserState.CHARSET:
if (ch in CHARSETS) {
cs = CHARSETS[ch];
if (ch === '/') { // ISOLatin is actually /A
this.skipNextChar();
}
} else {
cs = DEFAULT_CHARSET;
}
this._terminal.setgCharset(this._terminal.gcharset, cs);
this._terminal.gcharset = null;
this._state = ParserState.NORMAL;
break;
case ParserState.OSC:
// OSC Ps ; Pt ST
// OSC Ps ; Pt BEL
// Set Text Parameters.
if (ch === C0.ESC || ch === C0.BEL) {
if (ch === C0.ESC) this._position++;
this._terminal.params.push(this._terminal.currentParam);
switch (this._terminal.params[0]) {
case 0:
case 1:
case 2:
if (this._terminal.params[1]) {
this._terminal.title = this._terminal.params[1];
this._terminal.handleTitle(this._terminal.title);
}
break;
case 3:
// set X property
break;
case 4:
case 5:
// change dynamic colors
break;
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
case 18:
case 19:
// change dynamic ui colors
break;
case 46:
// change log file
break;
case 50:
// dynamic font
break;
case 51:
// emacs shell
break;
case 52:
// manipulate selection data
break;
case 104:
case 105:
case 110:
case 111:
case 112:
case 113:
case 114:
case 115:
case 116:
case 117:
case 118:
// reset colors
break;
}
this._terminal.params = [];
this._terminal.currentParam = 0;
this._state = ParserState.NORMAL;
} else {
if (!this._terminal.params.length) {
if (ch >= '0' && ch <= '9') {
this._terminal.currentParam =
this._terminal.currentParam * 10 + ch.charCodeAt(0) - 48;
} else if (ch === ';') {
this._terminal.params.push(this._terminal.currentParam);
this._terminal.currentParam = '';
}
} else {
this._terminal.currentParam += ch;
}
}
break;
case ParserState.CSI_PARAM:
if (ch in csiParamStateHandler) {
csiParamStateHandler[ch](this);
break;
}
this.finalizeParam();
// Fall through the CSI as this character should be the CSI code.
this._state = ParserState.CSI;
case ParserState.CSI:
if (ch in csiStateHandler) {
if (this._terminal.debug) {
this._terminal.log(`CSI ${this._terminal.prefix ? this._terminal.prefix : ''} ${this._terminal.params ? this._terminal.params.join(';') : ''} ${this._terminal.postfix ? this._terminal.postfix : ''} ${ch}`);
}
csiStateHandler[ch](this._inputHandler, this._terminal.params, this._terminal.prefix, this._terminal.postfix, this);
} else {
this._terminal.error('Unknown CSI code: %s.', ch);
}
this._state = ParserState.NORMAL;
this._terminal.prefix = '';
this._terminal.postfix = '';
break;
case ParserState.DCS:
if (ch === C0.ESC || ch === C0.BEL) {
if (ch === C0.ESC) this._position++;
let pt;
let valid: boolean;
switch (this._terminal.prefix) {
// User-Defined Keys (DECUDK).
case '':
break;
// Request Status String (DECRQSS).
// test: echo -e '\eP$q"p\e\\'
case '$q':
pt = this._terminal.currentParam;
valid = false;
switch (pt) {
// DECSCA
case '"q':
pt = '0"q';
break;
// DECSCL
case '"p':
pt = '61"p';
break;
// DECSTBM
case 'r':
pt = ''
+ (this._terminal.buffer.scrollTop + 1)
+ ';'
+ (this._terminal.buffer.scrollBottom + 1)
+ 'r';
break;
// SGR
case 'm':
pt = '0m';
break;
default:
this._terminal.error('Unknown DCS Pt: %s.', pt);
pt = '';
break;
}
this._terminal.send(C0.ESC + 'P' + +valid + '$r' + pt + C0.ESC + '\\');
break;
// Set Termcap/Terminfo Data (xterm, experimental).
case '+p':
break;
// Request Termcap/Terminfo String (xterm, experimental)
// Regular xterm does not even respond to this sequence.
// This can cause a small glitch in vim.
// test: echo -ne '\eP+q6b64\e\\'
case '+q':
pt = this._terminal.currentParam;
valid = false;
this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\');
break;
default:
this._terminal.error('Unknown DCS prefix: %s.', this._terminal.prefix);
break;
}
this._terminal.currentParam = 0;
this._terminal.prefix = '';
this._state = ParserState.NORMAL;
} else if (!this._terminal.currentParam) {
if (!this._terminal.prefix && ch !== '$' && ch !== '+') {
this._terminal.currentParam = ch;
} else if (this._terminal.prefix.length === 2) {
this._terminal.currentParam = ch;
} else {
this._terminal.prefix += ch;
}
} else {
this._terminal.currentParam += ch;
}
break;
case ParserState.IGNORE:
// For PM and APC.
if (ch === C0.ESC || ch === C0.BEL) {
if (ch === C0.ESC) this._position++;
this._state = ParserState.NORMAL;
}
break;
}
}
// Fire the cursormove event if it's moved. This is done inside the parser
// as a render cannot happen in the middle of a parsing round.
if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) {
this._terminal.emit('cursormove');
}
return this._state;
}
/**
* Set the parser's current parsing state.
*
* @param state The new state.
*/
public setState(state: ParserState): void {
this._state = state;
}
/**
* Sets the parsier's current prefix. CSI codes can have prefixes of '?', '>'
* or '!'.
*
* @param prefix The prefix.
*/
public setPrefix(prefix: string): void {
this._terminal.prefix = prefix;
}
/**
* Sets the parsier's current prefix. CSI codes can have postfixes of '$',
* '"', ' ', '\''.
*
* @param postfix The postfix.
*/
public setPostfix(postfix: string): void {
this._terminal.postfix = postfix;
}
/**
* Sets the parser's current parameter.
*
* @param param the parameter.
*/
public setParam(param: number): void {
this._terminal.currentParam = param;
}
/**
* Gets the parser's current parameter.
*/
public getParam(): number {
return this._terminal.currentParam;
}
/**
* Finalizes the parser's current parameter, adding it to the list of
* parameters and setting the new current parameter to 0.
*/
public finalizeParam(): void {
this._terminal.params.push(this._terminal.currentParam);
this._terminal.currentParam = 0;
}
/**
* Tell the parser to skip the next character.
*/
public skipNextChar(): void {
this._position++;
}
/**
* Tell the parser to repeat parsing the current character (for example if it
* needs parsing using a different state.
*/
// public repeatChar(): void {
// this._position--;
// }
}
+72 -3
View File
@@ -4,8 +4,8 @@
*/
import { assert } from 'chai';
import { CharMeasure } from './utils/CharMeasure';
import { SelectionManager } from './SelectionManager';
import { CharMeasure } from './ui/CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
@@ -25,6 +25,8 @@ class TestSelectionManager extends SelectionManager {
public get model(): SelectionModel { return this._model; }
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
@@ -51,7 +53,7 @@ describe('SelectionManager', () => {
});
function stringToRow(text: string): LineData {
let result: LineData = [];
const result: LineData = [];
for (let i = 0; i < text.length; i++) {
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
}
@@ -298,6 +300,16 @@ describe('SelectionManager', () => {
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
});
it('should select the entire wrapped line', () => {
buffer.lines.set(0, stringToRow('foo'));
const line2 = stringToRow('bar');
(<any>line2).isWrapped = true;
buffer.lines.set(1, line2);
selectionManager.selectLineAt(0);
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column');
});
});
describe('selectAll', () => {
@@ -368,4 +380,61 @@ describe('SelectionManager', () => {
assert.equal(selectionManager.hasSelection, true);
});
});
describe('column selection', () => {
it('should select a column of text', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('abcdefghij'));
buffer.lines.set(1, stringToRow('klmnopqrst'));
buffer.lines.set(2, stringToRow('uvwxyz'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [2, 0];
selectionManager.model.selectionEnd = [4, 2];
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
});
it('should select a column of text without chopping up double width characters', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('語'));
buffer.lines.set(2, stringToRow('b'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n語\nb');
});
it('should select a column of text with single character emojis', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a'));
buffer.lines.set(1, stringToRow('☃'));
buffer.lines.set(2, stringToRow('c'));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
});
it('should select a column of text with double character emojis', () => {
// TODO the case this is testing works for me in the demo webapp,
// but doing it programmatically fails.
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('a '));
buffer.lines.set(1, stringArrayToRow(['😁', ' ']));
buffer.lines.set(2, stringToRow('c '));
selectionManager.selectionMode = SelectionMode.COLUMN;
selectionManager.model.selectionStart = [0, 0];
selectionManager.model.selectionEnd = [1, 2];
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
});
});
});
+67 -30
View File
@@ -6,7 +6,7 @@
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -54,10 +54,11 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
const enum SelectionMode {
export const enum SelectionMode {
NORMAL,
WORD,
LINE
LINE,
COLUMN
}
/**
@@ -80,7 +81,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
/**
* The current selection mode.
*/
private _activeSelectionMode: SelectionMode;
protected _activeSelectionMode: SelectionMode;
/**
* A setInterval timer that is active while the mouse is down whose callback
@@ -116,6 +117,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
public dispose(): void {
super.dispose();
this._removeMouseDownListeners();
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
@@ -177,30 +183,43 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
return '';
}
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
let result: string[] = [];
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
const result: string[] = [];
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
if (this._activeSelectionMode === SelectionMode.COLUMN) {
// Ignore zero width selections
if (start[0] === end[0]) {
return '';
}
for (let i = start[1]; i <= end[1]; i++) {
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
result.push(lineText);
}
}
} else {
// Get first row
const startRowEndCol = start[1] === end[1] ? end[0] : null;
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
// Get middle rows
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = this._buffer.lines.get(i);
const lineText = this._buffer.translateBufferLineToString(i, true);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
// Get final row
if (start[1] !== end[1]) {
const bufferLine = this._buffer.lines.get(end[1]);
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if ((<any>bufferLine).isWrapped) {
result[result.length - 1] += lineText;
} else {
result.push(lineText);
}
}
}
@@ -249,7 +268,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
*/
private _refresh(): void {
this._refreshAnimationFrame = null;
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
this.emit('refresh', {
start: this._model.finalSelectionStart,
end: this._model.finalSelectionEnd,
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
});
}
/**
@@ -358,7 +381,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
public shouldForceSelection(event: MouseEvent): boolean {
return Browser.isMac ? event.altKey : event.shiftKey;
if (Browser.isMac) {
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
}
return event.shiftKey;
}
/**
@@ -449,7 +476,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _onSingleClick(event: MouseEvent): void {
this._model.selectionStartLength = 0;
this._model.isSelectAllActive = false;
this._activeSelectionMode = SelectionMode.NORMAL;
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
// Initialize the new selection
this._model.selectionStart = this._getMouseBufferCoords(event);
@@ -502,6 +529,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
}
}
/**
* Returns whether the selection manager should operate in column select mode
* @param event the mouse or keyboard event
*/
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
}
/**
* Handles the mousemove event when the mouse button is down, recording the
* end of the selection and refreshing the selection.
@@ -586,7 +621,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent): void {
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
@@ -803,7 +838,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param line The line index.
*/
protected _selectLineAt(line: number): void {
this._model.selectionStart = [0, line];
this._model.selectionStartLength = this._terminal.cols;
const wrappedRange = this._buffer.getWrappedRangeForLine(line);
this._model.selectionStart = [0, wrappedRange.first];
this._model.selectionEnd = [this._terminal.cols, wrappedRange.last];
this._model.selectionStartLength = 0;
}
}
+25 -19
View File
@@ -14,6 +14,11 @@ import * as pty from 'node-pty';
import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
import { IViewport } from './Types';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
}
let primitivePty: any;
@@ -24,8 +29,8 @@ let primitivePty: any;
function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
let b = new Buffer(64000);
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
const b = new Buffer(64000);
const bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
});
}
@@ -45,7 +50,7 @@ function formatError(input: string, output: string, expected: string): string {
return '\x1b[33m' + (' ' + counter).slice(-2) + color + s;
};
}
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
@@ -74,34 +79,34 @@ function terminalToString(term: Terminal): string {
// Skip tests on Windows since pty.open isn't supported
if (os.platform() !== 'win32') {
let CONSOLE_LOG = console.log;
const consoleLog = console.log;
// expect files need terminal at 80x25!
let COLS = 80;
let ROWS = 25;
const cols = 80;
const rows = 25;
/** 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
primitivePty = pty.native.open(COLS, ROWS);
primitivePty = (<any>pty).native.open(cols, rows);
/** tests */
describe('xterm output comparison', () => {
let xterm;
let xterm: TestTerminal;
beforeEach(() => {
xterm = new Terminal({ cols: COLS, rows: ROWS });
xterm = new TestTerminal({ cols: cols, rows: rows });
xterm.refresh = () => {};
xterm.viewport = {
xterm.viewport = <IViewport>{
syncScrollArea: () => {}
};
});
// omit stack trace for escape sequence files
Error.stackTraceLimit = 0;
let files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
// only successful tests for now
let skip = [
const skip = [
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
40, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
@@ -118,21 +123,22 @@ if (os.platform() !== 'win32') {
((filename: string) => {
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
let inFile = fs.readFileSync(filename, 'utf8');
const 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(fromPty);
xterm._innerWrite();
xterm.innerWrite();
const fromEmulator = terminalToString(xterm);
console.log = consoleLog;
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
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');
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(inFile, fromEmulator, expected));
@@ -155,7 +161,7 @@ describe('typings', () => {
tsc += '.cmd';
}
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
let result = cp.spawnSync(tsc, { cwd: fixtureDir });
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
// Clean up
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
+53 -297
View File
@@ -5,7 +5,6 @@
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import * as attach from './addons/attach/attach';
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
@@ -13,7 +12,6 @@ const INIT_COLS = 80;
const INIT_ROWS = 24;
class TestTerminal extends Terminal {
public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollLines: number} { return this._evaluateKeyEscapeSequence(<KeyboardEvent>ev); }
public keyDown(ev: any): boolean { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
}
@@ -53,13 +51,6 @@ describe('term.js addons', () => {
});
});
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
// prototype.
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
});
describe('getOption', () => {
it('should retrieve the option correctly', () => {
// In the `options` namespace.
@@ -77,13 +68,13 @@ describe('term.js addons', () => {
});
describe('attachCustomKeyEventHandler', () => {
let evKeyDown = <KeyboardEvent>{
const evKeyDown = <KeyboardEvent>{
preventDefault: () => {},
stopPropagation: () => {},
type: 'keydown',
keyCode: 77
};
let evKeyPress = <KeyboardEvent>{
const evKeyPress = <KeyboardEvent>{
preventDefault: () => {},
stopPropagation: () => {},
type: 'keypress',
@@ -129,9 +120,20 @@ describe('term.js addons', () => {
});
});
describe('reset', () => {
it('should not affect cursorState', () => {
term.cursorState = 1;
term.reset();
assert.equal(term.cursorState, 1);
term.cursorState = 0;
term.reset();
assert.equal(term.cursorState, 0);
});
});
describe('clear', () => {
it('should clear a buffer equal to rows', () => {
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
term.clear();
assert.equal(term.buffer.y, 0);
assert.equal(term.buffer.ybase, 0);
@@ -148,7 +150,7 @@ describe('term.js addons', () => {
term.write('test\n');
}
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
term.clear();
assert.equal(term.buffer.y, 0);
assert.equal(term.buffer.ybase, 0);
@@ -160,7 +162,7 @@ describe('term.js addons', () => {
}
});
it('should not break the prompt when cleared twice', () => {
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
term.clear();
term.clear();
assert.equal(term.buffer.y, 0);
@@ -176,7 +178,7 @@ describe('term.js addons', () => {
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 2; i++) {
term.writeln('test');
@@ -211,7 +213,7 @@ describe('term.js addons', () => {
});
describe('scrollPages', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -248,7 +250,7 @@ describe('term.js addons', () => {
});
describe('scrollToBottom', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -269,7 +271,7 @@ describe('term.js addons', () => {
});
describe('scrollToLine', () => {
let startYDisp;
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
@@ -296,22 +298,19 @@ describe('term.js addons', () => {
});
});
describe('keyDown', () => {
describe('keyPress', () => {
it('should scroll down, when a key is pressed and terminal is scrolled up', () => {
// Override _evaluateKeyEscapeSequence to return cancel code
(<any>term)._evaluateKeyEscapeSequence = () => {
return { key: 'a' };
};
let event = <KeyboardEvent>{
const event = <KeyboardEvent>{
type: 'keydown',
keyCode: 0,
key: 'a',
keyCode: 65,
preventDefault: () => {},
stopPropagation: () => {}
};
term.buffer.ydisp = 0;
term.buffer.ybase = 40;
term.keyDown(event);
term.keyPress(event);
// Ensure that now the terminal is scrolled to bottom
assert.equal(term.buffer.ydisp, term.buffer.ybase);
@@ -322,7 +321,7 @@ describe('term.js addons', () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
}
let startYDisp = (term.rows * 2) + 1;
const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => {
return false;
});
@@ -330,7 +329,7 @@ describe('term.js addons', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollLines(-1);
assert.equal(term.buffer.ydisp, startYDisp - 1);
term.keyDown(<KeyboardEvent>{ keyCode: 0 });
term.keyPress(<KeyboardEvent>{ keyCode: 0 });
assert.equal(term.buffer.ydisp, startYDisp - 1);
});
});
@@ -468,252 +467,9 @@ describe('term.js addons', () => {
});
});
describe('evaluateKeyEscapeSequence', () => {
it('should return the correct escape sequence for unmodified keys', () => {
// Backspace
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^?
// Tab
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t');
// Return/enter
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR
// Escape
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b');
// Page up, page down
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B
// Insert
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
// Delete
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
// F1-F12
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[3;5~ for ctrl+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
});
it('should return \\x1b[3;2~ for shift+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
});
it('should return \\x1b[3;3~ for alt+delete', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
});
it('should return \\x1b[5D for ctrl+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5A for ctrl+up', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for ctrl+down', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
describe('On non-macOS platforms', () => {
beforeEach(() => {
term.browser.isMac = false;
});
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
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', () => {
beforeEach(() => {
term.browser.isMac = true;
});
it('should return \\x1bb for alt+left', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D
});
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', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
});
describe('Third level shift', () => {
let evKeyDown;
let evKeyPress;
let evKeyDown: any;
let evKeyPress: any;
beforeEach(() => {
term.handler = () => {};
@@ -784,11 +540,11 @@ describe('term.js addons', () => {
});
it('should emit key with alt + key on keyPress', (done) => {
let keys = ['@', '@', '\\', '\\', '|', '|'];
const keys = ['@', '@', '\\', '\\', '|', '|'];
term.on('keypress', (key) => {
if (key) {
let index = keys.indexOf(key);
const index = keys.indexOf(key);
assert(index !== -1, 'Emitted wrong key: ' + key);
keys.splice(index, 1);
}
@@ -850,11 +606,11 @@ describe('term.js addons', () => {
});
it('should emit key with alt + ctrl + key on keyPress', (done) => {
let keys = ['@', '@', '\\', '\\', '|', '|'];
const keys = ['@', '@', '\\', '\\', '|', '|'];
term.on('keypress', (key) => {
if (key) {
let index = keys.indexOf(key);
const index = keys.indexOf(key);
assert(index !== -1, 'Emitted wrong key: ' + key);
keys.splice(index, 1);
}
@@ -895,10 +651,10 @@ describe('term.js addons', () => {
describe('unicode - surrogates', () => {
it('2 characters per cell', function (): void {
this.timeout(10000); // This is needed because istanbul patches code and slows it down
let high = String.fromCharCode(0xD800);
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high + String.fromCharCode(i));
let tchar = term.buffer.lines.get(0)[0];
const tchar = term.buffer.lines.get(0)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -907,7 +663,7 @@ describe('term.js addons', () => {
}
});
it('2 characters at last cell', () => {
let high = String.fromCharCode(0xD800);
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.write(high + String.fromCharCode(i));
@@ -918,7 +674,7 @@ describe('term.js addons', () => {
}
});
it('2 characters per cell over line end with autowrap', () => {
let high = String.fromCharCode(0xD800);
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = true;
@@ -931,7 +687,7 @@ describe('term.js addons', () => {
}
});
it('2 characters per cell over line end without autowrap', () => {
let high = String.fromCharCode(0xD800);
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.wraparoundMode = false;
@@ -944,11 +700,11 @@ describe('term.js addons', () => {
}
});
it('splitted surrogates', () => {
let high = String.fromCharCode(0xD800);
const high = String.fromCharCode(0xD800);
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high);
term.write(String.fromCharCode(i));
let tchar = term.buffer.lines.get(0)[0];
const tchar = term.buffer.lines.get(0)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -979,12 +735,12 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
}
let tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -993,12 +749,12 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
}
let tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
@@ -1021,7 +777,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1032,7 +788,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -1042,7 +798,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1067,7 +823,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1091,7 +847,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1102,7 +858,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
@@ -1112,7 +868,7 @@ describe('term.js addons', () => {
term.buffer.x = 1;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (!(i % 2)) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1136,7 +892,7 @@ describe('term.js addons', () => {
term.wraparoundMode = true;
term.write(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
let tchar = term.buffer.lines.get(0)[i];
const tchar = term.buffer.lines.get(0)[i];
if (i % 2) {
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
@@ -1147,7 +903,7 @@ describe('term.js addons', () => {
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
}
}
let tchar = term.buffer.lines.get(1)[0];
const tchar = term.buffer.lines.get(1)[0];
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
+209 -544
View File
File diff suppressed because it is too large Load Diff
+175 -24
View File
@@ -3,9 +3,10 @@
* @license MIT
*/
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
import { IMouseZoneManager } from './ui/Types';
import { ICharset } from './core/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
@@ -43,9 +44,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -87,7 +86,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
@@ -106,10 +105,11 @@ export interface ICompositionHelper {
}
/**
* Handles actions generated by the parser.
* Calls the parser and handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
parse(data: string): void;
print(data: string, start: number, end: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
@@ -135,26 +135,49 @@ export interface IInputHandler {
/** 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 T */ scrollDown(params?: number[], collect?: string): 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 a */ hPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(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 h */ setMode(params?: number[], collect?: string): void;
/** CSI l */ resetMode(params?: number[], collect?: string): 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 n */ deviceStatus(params?: number[], collect?: string): void;
/** CSI p */ softReset(params?: number[], collect?: string): void;
/** CSI q */ setCursorStyle(params?: number[], collect?: string): void;
/** CSI r */ setScrollRegion(params?: number[], collect?: string): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
/** OSC 0
OSC 2 */ setTitle(data: string): void;
/** ESC E */ nextLine(): void;
/** ESC = */ keypadApplicationMode(): void;
/** ESC > */ keypadNumericMode(): void;
/** ESC % G
ESC % @ */ selectDefaultCharset(): void;
/** ESC ( C
ESC ) C
ESC * C
ESC + C
ESC - C
ESC . C
ESC / C */ selectCharset(collectAndFlag: string): void;
/** ESC D */ index(): void;
/** ESC H */ tabSet(): void;
/** ESC M */ reverseIndex(): void;
/** ESC c */ reset(): void;
/** ESC n
ESC o
ESC |
ESC }
ESC ~ */ setgLevel(level: number): void;
}
export interface ILinkMatcher {
@@ -169,10 +192,6 @@ export interface ILinkMatcher {
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x1: number;
y1: number;
@@ -190,7 +209,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
buffers: IBufferSet;
@@ -226,7 +244,7 @@ export interface ILinkifierAccessor {
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
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 };
}
@@ -238,6 +256,7 @@ export interface ICharMeasure {
// TODO: The options that are not in the public API should be reviewed
export interface ITerminalOptions extends IPublicTerminalOptions {
[key: string]: any;
cancelEvents?: boolean;
convertEol?: boolean;
debug?: boolean;
@@ -261,6 +280,7 @@ export interface IBuffer {
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
getWrappedRangeForLine(y: number): { first: number, last: number };
nextStop(x?: number): number;
prevStop(x?: number): number;
}
@@ -355,3 +375,134 @@ export interface IBrowser {
export interface ISoundManager {
playBellSound(): void;
}
/**
* Internal states of EscapeSequenceParser.
*/
export const enum ParserState {
GROUND = 0,
ESCAPE = 1,
ESCAPE_INTERMEDIATE = 2,
CSI_ENTRY = 3,
CSI_PARAM = 4,
CSI_INTERMEDIATE = 5,
CSI_IGNORE = 6,
SOS_PM_APC_STRING = 7,
OSC_STRING = 8,
DCS_ENTRY = 9,
DCS_PARAM = 10,
DCS_IGNORE = 11,
DCS_INTERMEDIATE = 12,
DCS_PASSTHROUGH = 13
}
/**
* Internal actions of EscapeSequenceParser.
*/
export const enum ParserAction {
IGNORE = 0,
ERROR = 1,
PRINT = 2,
EXECUTE = 3,
OSC_START = 4,
OSC_PUT = 5,
OSC_END = 6,
CSI_DISPATCH = 7,
PARAM = 8,
COLLECT = 9,
ESC_DISPATCH = 10,
CLEAR = 11,
DCS_HOOK = 12,
DCS_PUT = 13,
DCS_UNHOOK = 14
}
/**
* Internal state of EscapeSequenceParser.
* Used as argument of the error handler to allow
* introspection at runtime on parse errors.
* Return it with altered values to recover from
* faulty states (not yet supported).
* Set `abort` to `true` to abort the current parsing.
*/
export interface IParsingState {
// position in parse string
position: number;
// actual character code
code: number;
// current parser state
currentState: ParserState;
// print buffer start index (-1 for not set)
print: number;
// dcs buffer start index (-1 for not set)
dcs: number;
// osc string buffer
osc: string;
// collect buffer with intermediate characters
collect: string;
// params buffer
params: number[];
// should abort (default: false)
abort: boolean;
}
/**
* DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate
* subparsers that get hook/unhooked and can handle
* arbitrary amount of print data.
* On entering a DSC sequence `hook` is called by
* `EscapeSequenceParser`. Use it to initialize or reset
* states needed to handle the current DCS sequence.
* EscapeSequenceParser will call `put` several times if the
* parsed string got splitted, therefore you might have to collect
* `data` until `unhook` is called. `unhook` marks the end
* of the current DCS sequence.
*/
export interface IDcsHandler {
hook(collect: string, params: number[], flag: number): void;
put(data: string, start: number, end: number): void;
unhook(): void;
}
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser extends IDisposable {
/**
* Reset the parser to its initial state (handlers are kept).
*/
reset(): void;
/**
* Parse string `data`.
* @param data The data to parse.
*/
parse(data: string): void;
setPrintHandler(callback: (data: string, start: number, end: number) => void): void;
clearPrintHandler(): void;
setExecuteHandler(flag: string, callback: () => void): void;
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(callback: (code: number) => void): void;
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
setOscHandler(ident: number, callback: (data: string) => void): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
clearDcsHandler(collectAndFlag: string): void;
setDcsHandlerFallback(handler: IDcsHandler): void;
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}
+8 -4
View File
@@ -5,7 +5,9 @@
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { CharMeasure } from './ui/CharMeasure';
import { Disposable } from './common/Lifecycle';
import { addDisposableDomListener } from './ui/Lifecycle';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -13,7 +15,7 @@ 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 {
export class Viewport extends Disposable implements IViewport {
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
@@ -39,11 +41,13 @@ export class Viewport implements IViewport {
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
super();
// 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));
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
@@ -180,7 +184,7 @@ export class Viewport implements IViewport {
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): void {
let deltaY = this._lastTouchY - ev.touches[0].pageY;
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return;
+5 -1
View File
@@ -5,9 +5,13 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
_core: {
register<T extends IDisposable>(d: T): void;
};
__socket?: WebSocket;
__attachSocketBuffer?: string;
+23 -10
View File
@@ -5,9 +5,7 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { Terminal, IDisposable } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
@@ -38,10 +36,11 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
}
};
let myTextDecoder;
// TODO: This should be typed but there seem to be issues importing the type
let myTextDecoder: any;
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
let str: string;
if (typeof ev.data === 'object') {
if (!myTextDecoder) {
@@ -51,7 +50,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
str = myTextDecoder.decode(ev.data);
displayData(str);
} else {
let fileReader = new FileReader();
const fileReader = new FileReader();
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(this.result);
@@ -88,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
socket.send(data);
};
socket.addEventListener('message', addonTerminal.__getMessage);
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
if (bidirectional) {
addonTerminal.on('data', addonTerminal.__sendData);
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
}
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));
}
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {
socket.addEventListener(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
socket.removeEventListener(type, handler);
handler = null;
}
};
}
/**
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+3
View File
@@ -0,0 +1,3 @@
## fit addon
The fit addon adjusts the dimensions of the terminal to match best fit its parent element container. `fit` will only work when the element is visible.
+4 -6
View File
@@ -13,8 +13,6 @@
* row and truncate its width with the current number of columns).
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
export interface IGeometry {
@@ -39,10 +37,10 @@ export function proposeGeometry(term: Terminal): IGeometry {
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 availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
};
return geometry;
}
@@ -52,7 +50,7 @@ export function fit(term: Terminal): void {
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
(<any>term).renderer.clear();
(<any>term)._core.renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
-2
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+7 -4
View File
@@ -5,14 +5,17 @@
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
// TODO: Don't rely on this private API
export interface ITerminalCore {
buffer: any;
selectionManager: any;
}
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
_core: ITerminalCore;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
+12 -12
View File
@@ -35,14 +35,14 @@ export class SearchHelper implements ISearchHelper {
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionEnd) {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionEnd[1];
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal.buffer.ybase + this._terminal.rows; y++) {
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y);
if (result) {
break;
@@ -76,10 +76,10 @@ export class SearchHelper implements ISearchHelper {
let result: ISearchResult;
let startRow = this._terminal.buffer.ydisp;
if (this._terminal.selectionManager.selectionStart) {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal.selectionManager.selectionStart[1];
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
// Search from ydisp + 1 to end
@@ -92,7 +92,7 @@ export class SearchHelper implements ISearchHelper {
// Search from the top to the current ydisp
if (!result) {
for (let y = this._terminal.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
result = this._findInLine(term, y);
if (result) {
break;
@@ -111,11 +111,11 @@ export class SearchHelper implements ISearchHelper {
* @return The search result if it was found.
*/
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
const line = this._terminal.buffer.lines.get(y);
const line = this._terminal._core.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
@@ -147,8 +147,8 @@ export class SearchHelper implements ISearchHelper {
if (!result) {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
return true;
}
}
-2
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
-2
View File
@@ -6,8 +6,6 @@
* WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { ITerminadoAddonTerminal } from './Interfaces';
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+2 -2
View File
@@ -34,8 +34,8 @@ describe('webLinks addon', () => {
const row = ' http://foo.com/a~b#c~d?e~f ';
let match = row.match(term.regex);
let uri = match[term.options.matchIndex];
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
-2
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal, ILinkMatcherOptions } from 'xterm';
const protocolClause = '(https?:\\/\\/)';
+5 -1
View File
@@ -5,6 +5,10 @@
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
export interface ITerminalCore {
buffer: any;
}
export interface IWinptyCompatAddonTerminal extends Terminal {
_core: ITerminalCore;
}
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+2 -4
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
@@ -28,11 +26,11 @@ export function winptyCompatInit(terminal: Terminal): void {
// 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 line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.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);
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
+7 -2
View File
@@ -6,6 +6,11 @@
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+3 -5
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
@@ -48,13 +46,13 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
let zsentry;
function _shouldWrite(): boolean {
function shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new zmodem.Sentry({
to_terminal: (octets: ArrayLike<number>) => {
if (_shouldWrite()) {
if (shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
@@ -72,7 +70,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
// may be specific to xterm.js’s demo, ultimately we
// should reject anything that isn’t binary.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
if (shouldWrite()) {
term.write(evt.data);
}
}
@@ -36,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
// Reconstruct array, starting at index 0. Only transfer values from the
// indexes 0 to length.
let newArray = new Array<T>(newMaxLength);
const newArray = new Array<T>(newMaxLength);
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
newArray[i] = this._array[this._getCyclicIndex(i)];
}
+33
View File
@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'xterm';
/**
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
* object and its components.
*/
export abstract class Disposable implements IDisposable {
protected _disposables: IDisposable[] = [];
constructor() {
}
/**
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
*/
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
}
/**
* Registers a disposable object.
* @param d The disposable to register.
*/
public register<T extends IDisposable>(d: T): void {
this._disposables.push(d);
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
/**
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
* this event.
*/
export interface IKeyboardEvent {
altKey: boolean;
ctrlKey: boolean;
shiftKey: boolean;
metaKey: boolean;
keyCode: number;
key: string;
type: string;
}
@@ -77,3 +77,74 @@ export namespace C0 {
/** Delete (Caret = ^?) */
export const DEL = '\x7f';
}
/**
* C1 control codes
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
*/
export namespace C1 {
/** padding character */
export const PAD = '\x80';
/** High Octet Preset */
export const HOP = '\x81';
/** Break Permitted Here */
export const BPH = '\x82';
/** No Break Here */
export const NBH = '\x83';
/** Index */
export const IND = '\x84';
/** Next Line */
export const NEL = '\x85';
/** Start of Selected Area */
export const SSA = '\x86';
/** End of Selected Area */
export const ESA = '\x87';
/** Horizontal Tabulation Set */
export const HTS = '\x88';
/** Horizontal Tabulation With Justification */
export const HTJ = '\x89';
/** Vertical Tabulation Set */
export const VTS = '\x8a';
/** Partial Line Down */
export const PLD = '\x8b';
/** Partial Line Up */
export const PLU = '\x8c';
/** Reverse Index */
export const RI = '\x8d';
/** Single-Shift 2 */
export const SS2 = '\x8e';
/** Single-Shift 3 */
export const SS3 = '\x8f';
/** Device Control String */
export const DCS = '\x90';
/** Private Use 1 */
export const PU1 = '\x91';
/** Private Use 2 */
export const PU2 = '\x92';
/** Set Transmit State */
export const STS = '\x93';
/** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */
export const CCH = '\x94';
/** Message Waiting */
export const MW = '\x95';
/** Start of Protected Area */
export const SPA = '\x96';
/** End of Protected Area */
export const EPA = '\x97';
/** Start of String */
export const SOS = '\x98';
/** Single Graphic Character Introducer */
export const SGCI = '\x99';
/** Single Character Introducer */
export const SCI = '\x9a';
/** Control Sequence Introducer */
export const CSI = '\x9b';
/** String Terminator */
export const ST = '\x9c';
/** Operating System Command */
export const OSC = '\x9d';
/** Privacy Message */
export const PM = '\x9e';
/** Application Program Command */
export const APC = '\x9f';
}
+21
View File
@@ -0,0 +1,21 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export const enum KeyboardResultType {
SEND_KEY,
SELECT_ALL,
PAGE_UP,
PAGE_DOWN
}
export interface IKeyboardResult {
type: KeyboardResultType;
cancel: boolean;
key: string | undefined;
}
export interface ICharset {
[key: string]: string;
}
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ICharset } from './Types';
import { ICharset } from '../Types';
/**
* The character sets supported by the terminal. These enable several languages
+284
View File
@@ -0,0 +1,284 @@
import { assert } from 'chai';
import { evaluateKeyboardEvent } from './Keyboard';
import { IKeyboardResult } from '../Types';
/**
* A helper function for testing which allows passing in a partial event and defaults will be filled
* in on it.
*/
function testEvaluateKeyboardEvent(partialEvent: {
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
keyCode?: number;
key?: string;
type?: string;
}, partialOptions: {
applicationCursorMode?: boolean;
isMac?: boolean;
macOptionIsMeta?: boolean;
} = {}): IKeyboardResult {
const event = {
altKey: partialEvent.altKey || false,
ctrlKey: partialEvent.ctrlKey || false,
shiftKey: partialEvent.shiftKey || false,
metaKey: partialEvent.metaKey || false,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : undefined,
key: partialEvent.key || '',
type: partialEvent.type || ''
};
const options = {
applicationCursorMode: partialOptions.applicationCursorMode || false,
isMac: partialOptions.isMac || false,
macOptionIsMeta: partialOptions.macOptionIsMeta || false
};
return evaluateKeyboardEvent(event, options.applicationCursorMode, options.isMac, options.macOptionIsMeta);
}
describe('Keyboard', () => {
describe('evaluateKeyEscapeSequence', () => {
it('should return the correct escape sequence for unmodified keys', () => {
// Backspace
assert.equal(testEvaluateKeyboardEvent({ keyCode: 8 }).key, '\x7f'); // ^?
// Tab
assert.equal(testEvaluateKeyboardEvent({ keyCode: 9 }).key, '\t');
// Return/enter
assert.equal(testEvaluateKeyboardEvent({ keyCode: 13 }).key, '\r'); // CR
// Escape
assert.equal(testEvaluateKeyboardEvent({ keyCode: 27 }).key, '\x1b');
// Page up, page down
assert.equal(testEvaluateKeyboardEvent({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(testEvaluateKeyboardEvent({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(testEvaluateKeyboardEvent({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(testEvaluateKeyboardEvent({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(testEvaluateKeyboardEvent({ keyCode: 38 }).key, '\x1b[A'); // CSI A
assert.equal(testEvaluateKeyboardEvent({ keyCode: 39 }).key, '\x1b[C'); // CSI C
assert.equal(testEvaluateKeyboardEvent({ keyCode: 40 }).key, '\x1b[B'); // CSI B
// Insert
assert.equal(testEvaluateKeyboardEvent({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
// Delete
assert.equal(testEvaluateKeyboardEvent({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
// F1-F12
assert.equal(testEvaluateKeyboardEvent({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
assert.equal(testEvaluateKeyboardEvent({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
assert.equal(testEvaluateKeyboardEvent({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
assert.equal(testEvaluateKeyboardEvent({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
assert.equal(testEvaluateKeyboardEvent({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[3;5~ for ctrl+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
});
it('should return \\x1b[3;2~ for shift+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
});
it('should return \\x1b[3;3~ for alt+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
});
it('should return \\x1b[5D for ctrl+left', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5A for ctrl+up', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for ctrl+down', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
describe('On non-macOS platforms', () => {
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
it('should return \\x1bb for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
});
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
});
});
describe('with macOptionIsMeta', () => {
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true, macOptionIsMeta: true }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
it('should handle mobile arrow events', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }).key, '\x1b[A');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }, { applicationCursorMode: true }).key, '\x1bOA');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }).key, '\x1b[D');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }, { applicationCursorMode: true }).key, '\x1bOD');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }).key, '\x1b[C');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }, { applicationCursorMode: true }).key, '\x1bOC');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
});
});
});
+357
View File
@@ -0,0 +1,357 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
import { IKeyboardEvent } from '../../common/Types';
import { IKeyboardResult, KeyboardResultType } from '../Types';
import { C0 } from '../../common/data/EscapeSequences';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = {
// digits 0-9
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
// special chars
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ['\'', '"']
};
export function evaluateKeyboardEvent(
ev: IKeyboardEvent,
applicationCursorMode: boolean,
isMac: boolean,
macOptionIsMeta: boolean
): IKeyboardResult {
const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY,
// Whether to cancel event propogation (NOTE: this may not be needed since the event is
// canceled at the end of keyDown
cancel: false,
// The new key even to emit
key: undefined
};
const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0);
switch (ev.keyCode) {
case 0:
if (ev.key === 'UIKeyInputUpArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
}
else if (ev.key === 'UIKeyInputLeftArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
}
else if (ev.key === 'UIKeyInputRightArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
}
else if (ev.key === 'UIKeyInputDownArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
}
break;
case 8:
// backspace
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;
case 9:
// tab
if (ev.shiftKey) {
result.key = C0.ESC + '[Z';
break;
}
result.key = C0.HT;
result.cancel = true;
break;
case 13:
// return/enter
result.key = C0.CR;
result.cancel = true;
break;
case 27:
// escape
result.key = C0.ESC;
result.cancel = true;
break;
case 37:
// left-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
break;
case 39:
// right-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
break;
case 38:
// up-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
break;
case 40:
// down-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
break;
case 45:
// insert
if (!ev.shiftKey && !ev.ctrlKey) {
// <Ctrl> or <Shift> + <Insert> are used to
// copy-paste on some systems.
result.key = C0.ESC + '[2~';
}
break;
case 46:
// delete
if (modifiers) {
result.key = C0.ESC + '[3;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[3~';
}
break;
case 36:
// home
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OH';
} else {
result.key = C0.ESC + '[H';
}
break;
case 35:
// end
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OF';
} else {
result.key = C0.ESC + '[F';
}
break;
case 33:
// page up
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_UP;
} else {
result.key = C0.ESC + '[5~';
}
break;
case 34:
// page down
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_DOWN;
} else {
result.key = C0.ESC + '[6~';
}
break;
case 112:
// F1-F12
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P';
} else {
result.key = C0.ESC + 'OP';
}
break;
case 113:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q';
} else {
result.key = C0.ESC + 'OQ';
}
break;
case 114:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R';
} else {
result.key = C0.ESC + 'OR';
}
break;
case 115:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S';
} else {
result.key = C0.ESC + 'OS';
}
break;
case 116:
if (modifiers) {
result.key = C0.ESC + '[15;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[15~';
}
break;
case 117:
if (modifiers) {
result.key = C0.ESC + '[17;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[17~';
}
break;
case 118:
if (modifiers) {
result.key = C0.ESC + '[18;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[18~';
}
break;
case 119:
if (modifiers) {
result.key = C0.ESC + '[19;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[19~';
}
break;
case 120:
if (modifiers) {
result.key = C0.ESC + '[20;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[20~';
}
break;
case 121:
if (modifiers) {
result.key = C0.ESC + '[21;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[21~';
}
break;
case 122:
if (modifiers) {
result.key = C0.ESC + '[23;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[23~';
}
break;
case 123:
if (modifiers) {
result.key = C0.ESC + '[24;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[24~';
}
break;
default:
// a-z and space
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode === 32) {
// NUL
result.key = String.fromCharCode(0);
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
// escape, file sep, group sep, record sep, unit sep
result.key = String.fromCharCode(ev.keyCode - 51 + 27);
} else if (ev.keyCode === 56) {
// delete
result.key = String.fromCharCode(127);
} else if (ev.keyCode === 219) {
// ^[ - Control Sequence Introducer (CSI)
result.key = String.fromCharCode(27);
} else if (ev.keyCode === 220) {
// ^\ - String Terminator (ST)
result.key = String.fromCharCode(28);
} else if (ev.keyCode === 221) {
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
if (key) {
result.key = C0.ESC + key;
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
result.key = C0.ESC + String.fromCharCode(keyCode);
}
} else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL;
}
}
break;
}
return result;
}
+11 -11
View File
@@ -4,7 +4,7 @@
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
import { C0 } from '../common/data/EscapeSequences';
const enum Direction {
UP = 'A',
@@ -28,7 +28,7 @@ export class AltClickHandler {
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
let coordinates = this._terminal.mouseHelper.getCoords(
const coordinates = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
@@ -89,10 +89,10 @@ export class AltClickHandler {
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
const endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
}
@@ -108,8 +108,8 @@ export class AltClickHandler {
startRow = this._startRow;
}
let endRow = this._endRow;
let direction = this._horizontalDirection();
const endRow = this._endRow;
const direction = this._horizontalDirection();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow,
@@ -118,7 +118,7 @@ export class AltClickHandler {
}
private _moveHorizontallyOnly(): string {
let direction = this._horizontalDirection();
const direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
@@ -132,11 +132,11 @@ export class AltClickHandler {
*/
private _wrappedRowsCount(): number {
let wrappedRows = 0;
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
const 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;
const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++;
+1 -1
View File
@@ -58,7 +58,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
let text: string;
let dispatchPaste = function(text: string): void {
const dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, term.bracketedPasteMode);
term.handler(text);
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { Terminal } from './Terminal';
import * as attach from '../addons/attach/attach';
describe('Terminal', () => {
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
// prototype.
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
});
});
+148
View File
@@ -0,0 +1,148 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm';
import { ITerminal } from '../Types';
import { Terminal as TerminalCore } from '../Terminal';
import * as Strings from '../Strings';
export class Terminal implements ITerminalApi {
private _core: ITerminal;
constructor(options?: ITerminalOptions) {
this._core = new TerminalCore(options);
}
public get element(): HTMLElement { return this._core.element; }
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
public get markers(): IMarker[] { return this._core.markers; }
public blur(): void {
this._core.blur();
}
public focus(): void {
this._core.focus();
}
public on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void;
public on(type: 'data', listener: (...args: any[]) => void): void;
public on(type: 'key', listener: (key?: string, event?: KeyboardEvent) => void): void;
public on(type: 'keypress' | 'keydown', listener: (event?: KeyboardEvent) => void): void;
public on(type: 'refresh', listener: (data?: { start: number; end: number; }) => void): void;
public on(type: 'resize', listener: (data?: { cols: number; rows: number; }) => void): void;
public on(type: 'scroll', listener: (ydisp?: number) => void): void;
public on(type: 'title', listener: (title?: string) => void): void;
public on(type: string, listener: (...args: any[]) => void): void;
public on(type: any, listener: any): void {
this._core.on(type, listener);
}
public off(type: string, listener: (...args: any[]) => void): void {
this._core.off(type, listener);
}
public emit(type: string, data?: any): void {
this._core.emit(type, data);
}
public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable {
return this._core.addDisposableListener(type, handler);
}
public resize(columns: number, rows: number): void {
this._core.resize(columns, rows);
}
public writeln(data: string): void {
this._core.writeln(data);
}
public open(parent: HTMLElement): void {
this._core.open(parent);
}
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
return this._core.registerLinkMatcher(regex, handler, options);
}
public deregisterLinkMatcher(matcherId: number): void {
this._core.deregisterLinkMatcher(matcherId);
}
public addMarker(cursorYOffset: number): IMarker {
return this._core.addMarker(cursorYOffset);
}
public hasSelection(): boolean {
return this._core.hasSelection();
}
public getSelection(): string {
return this._core.getSelection();
}
public clearSelection(): void {
this._core.clearSelection();
}
public selectAll(): void {
this._core.selectAll();
}
public selectLines(start: number, end: number): void {
this._core.selectLines(start, end);
}
public dispose(): void {
this._core.dispose();
}
public destroy(): void {
this._core.destroy();
}
public scrollLines(amount: number): void {
this._core.scrollLines(amount);
}
public scrollPages(pageCount: number): void {
this._core.scrollPages(pageCount);
}
public scrollToTop(): void {
this._core.scrollToTop();
}
public scrollToBottom(): void {
this._core.scrollToBottom();
}
public scrollToLine(line: number): void {
this._core.scrollToLine(line);
}
public clear(): void {
this._core.clear();
}
public write(data: string): void {
this._core.write(data);
}
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
public getOption(key: 'colors'): string[];
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
public getOption(key: 'handler'): (data: string) => void;
public getOption(key: string): any;
public getOption(key: any): any {
return this._core.getOption(key);
}
public setOption(key: 'bellSound' | 'fontFamily' | 'termName', value: string): void;
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
public setOption(key: 'colors', value: string[]): void;
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
public setOption(key: 'handler', value: (data: string) => void): void;
public setOption(key: 'theme', value: ITheme): void;
public setOption(key: 'cols' | 'rows', value: number): void;
public setOption(key: string, value: any): void;
public setOption(key: any, value: any): void {
this._core.setOption(key, value);
}
public refresh(start: number, end: number): void {
this._core.refresh(start, end);
}
public reset(): void {
this._core.reset();
}
public static applyAddon(addon: any): void {
addon.apply(Terminal);
}
public static get strings(): ILocalizableStrings {
return Strings;
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public onFocus(terminal: ITerminal): void {}
public onCursorMove(terminal: ITerminal): void {}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
this._refreshCharAtlas(terminal, colorSet);
+2 -2
View File
@@ -33,10 +33,10 @@ describe('ColorManager', () => {
describe('constructor', () => {
it('should fill all colors with values', () => {
for (let key of Object.keys(cm.colors)) {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi') {
// A #rrggbb or rgba(...)
assert.ok(cm.colors[key].css.length >= 7);
assert.ok((<any>cm.colors)[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
+2 -2
View File
@@ -42,7 +42,7 @@ export const DEFAULT_ANSI_COLORS = (() => {
// Fill in the remaining 240 ANSI colors.
// Generate colors (16-231)
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
@@ -75,7 +75,7 @@ function fromHex(css: string): IColor {
}
function toPaddedHex(c: number): string {
let s = c.toString(16);
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
+6 -4
View File
@@ -11,8 +11,8 @@ import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { RenderDebouncer } from '../ui/RenderDebouncer';
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer {
@@ -62,12 +62,14 @@ export class Renderer extends EventEmitter implements IRenderer {
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
this.register(this._screenDprMonitor);
// 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);
this.register({ dispose: () => observer.disconnect() });
}
}
@@ -141,8 +143,8 @@ export class Renderer extends EventEmitter implements IRenderer {
this._runOperation(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
}
public onCursorMove(): void {
+56 -24
View File
@@ -7,39 +7,46 @@ import { ITerminal } from '../Types';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
interface ISelectionState {
start: [number, number];
end: [number, number];
columnSelectMode: boolean;
ydisp: number;
}
export class SelectionRenderLayer extends BaseRenderLayer {
private _state: {start: [number, number], end: [number, number]};
private _state: ISelectionState;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'selection', zIndex, true, colors);
this._clearState();
}
private _clearState(): void {
this._state = {
start: null,
end: null
end: null,
columnSelectMode: null,
ydisp: null
};
}
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
end: null
};
this._clearState();
}
public reset(terminal: ITerminal): void {
if (this._state.start && this._state.end) {
this._state = {
start: null,
end: null
};
this._clearState();
this.clearAll();
}
}
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Selection has not changed
if (this._state.start === start || this._state.end === end) {
if (!this._didStateChange(start, end, columnSelectMode, terminal.buffer.ydisp)) {
return;
}
@@ -62,25 +69,50 @@ export class SelectionRenderLayer extends BaseRenderLayer {
return;
}
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this._ctx.fillStyle = this._colors.selection.css;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
if (columnSelectMode) {
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const width = end[0] - startCol;
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
this.fillCells(startCol, viewportCappedStartRow, width, height);
} else {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 1);
// Draw middle rows
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
this.fillCells(0, viewportCappedEndRow, endCol, 1);
}
}
// Save state for next render
this._state.start = [start[0], start[1]];
this._state.end = [end[0], end[1]];
this._state.columnSelectMode = columnSelectMode;
this._state.ydisp = terminal.buffer.ydisp;
}
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean, ydisp: number): boolean {
return !this._areCoordinatesEqual(start, this._state.start) ||
!this._areCoordinatesEqual(end, this._state.end) ||
columnSelectMode !== this._state.columnSelectMode ||
ydisp !== this._state.ydisp;
}
private _areCoordinatesEqual(coord1: [number, number], coord2: [number, number]): boolean {
if (!coord1 || !coord2) {
return false;
}
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
}
}
+3 -1
View File
@@ -208,7 +208,9 @@ export class TextRenderLayer extends BaseRenderLayer {
return;
}
this._charAtlas.beginFrame();
if (this._charAtlas) {
this._charAtlas.beginFrame();
}
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
this._drawBackground(terminal, firstRow, lastRow);
+9 -4
View File
@@ -4,7 +4,7 @@
*/
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
@@ -20,17 +20,22 @@ export const enum FLAGS {
ITALIC = 64
}
export interface IRenderer extends IEventEmitter {
/**
* Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen.
*/
export interface IRenderer extends IEventEmitter, IDisposable {
dimensions: IRenderDimensions;
colorManager: IColorManager;
dispose(): void;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
@@ -94,7 +99,7 @@ export interface IRenderLayer {
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
/**
* Resize the render layer.
+1 -1
View File
@@ -9,7 +9,7 @@ import { ICharAtlasConfig } from '../../shared/atlas/Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors = {
const clonedColors = <IColorSet>{
foreground: colors.foreground,
background: colors.background,
cursor: null,
+1 -1
View File
@@ -11,7 +11,7 @@ interface ILinkedListNode<T> {
}
export default class LRUMap<T> {
private _map = {};
private _map: { [key: string]: ILinkedListNode<T> } = {};
private _head: ILinkedListNode<T> = null;
private _tail: ILinkedListNode<T> = null;
private _nodePool: ILinkedListNode<T>[] = [];
+10 -1
View File
@@ -19,10 +19,14 @@ export default class StaticCharAtlas extends BaseCharAtlas {
const canvas = this._document.createElement('canvas');
canvas.width = width;
canvas.height = height;
// This is useful for debugging
// document.body.appendChild(canvas);
return canvas;
}
public _doWarmUp(): void {
protected _doWarmUp(): void {
const result = generateStaticCharAtlasTexture(window, this._canvasFactory, this._config);
if (result instanceof HTMLCanvasElement) {
this._texture = result;
@@ -65,6 +69,9 @@ export default class StaticCharAtlas extends BaseCharAtlas {
if (!this._isCached(glyph, colorIndex)) {
return false;
}
ctx.save();
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this._config.scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._config.scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
@@ -86,6 +93,8 @@ export default class StaticCharAtlas extends BaseCharAtlas {
this._config.scaledCharHeight
);
ctx.restore();
return true;
}
}
+322
View File
@@ -0,0 +1,322 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IRenderer, IRenderDimensions, IColorSet } from '../Types';
import { ITerminal } from '../../Types';
import { ITheme } from 'xterm';
import { EventEmitter } from '../../EventEmitter';
import { ColorManager } from '../ColorManager';
import { RenderDebouncer } from '../../ui/RenderDebouncer';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, DomRendererRowFactory } from './DomRendererRowFactory';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows';
const FG_CLASS_PREFIX = 'xterm-fg-';
const BG_CLASS_PREFIX = 'xterm-bg-';
const FOCUS_CLASS = 'xterm-focus';
const SELECTION_CLASS = 'xterm-selection';
let nextTerminalId = 1;
// TODO: Pull into an addon when TS composite projects allow easier sharing of code (not just
// interfaces) between core and addons
/**
* A fallback renderer for when canvas is slow. This is not meant to be
* particularly fast or feature complete, more just stable and usable for when
* canvas is not an option.
*/
export class DomRenderer extends EventEmitter implements IRenderer {
private _renderDebouncer: RenderDebouncer;
private _rowFactory: DomRendererRowFactory;
private _terminalClass: number = nextTerminalId++;
private _themeStyleElement: HTMLStyleElement;
private _dimensionsStyleElement: HTMLStyleElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement;
public dimensions: IRenderDimensions;
public colorManager: ColorManager;
constructor(private _terminal: ITerminal, theme: ITheme | undefined) {
super();
const allowTransparency = this._terminal.options.allowTransparency;
this.colorManager = new ColorManager(document, allowTransparency);
this.setTheme(theme);
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true');
this._refreshRowElements(this._terminal.rows, this._terminal.cols);
this._selectionContainer = document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true');
this.dimensions = {
scaledCharWidth: null,
scaledCharHeight: null,
scaledCellWidth: null,
scaledCellHeight: null,
scaledCharLeft: null,
scaledCharTop: null,
scaledCanvasWidth: null,
scaledCanvasHeight: null,
canvasWidth: null,
canvasHeight: null,
actualCellWidth: null,
actualCellHeight: null
};
this._updateDimensions();
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._rowFactory = new DomRendererRowFactory(document);
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.appendChild(this._rowContainer);
this._terminal.screenElement.appendChild(this._selectionContainer);
}
private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = this._terminal.charMeasure.height * window.devicePixelRatio;
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth;
this.dimensions.scaledCellHeight = this.dimensions.scaledCharHeight;
this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows;
this.dimensions.canvasWidth = this._terminal.charMeasure.width * this._terminal.cols;
this.dimensions.canvasHeight = this._terminal.charMeasure.height * this._terminal.rows;
this.dimensions.actualCellWidth = this._terminal.charMeasure.width;
this.dimensions.actualCellHeight = this._terminal.charMeasure.height;
this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`;
element.style.height = `${this._terminal.charMeasure.height}px`;
});
if (!this._dimensionsStyleElement) {
this._dimensionsStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._dimensionsStyleElement);
}
const styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} span {` +
` display: inline-block;` +
` height: 100%;` +
` vertical-align: top;` +
` width: ${this._terminal.charMeasure.width}px` +
`}`;
this._dimensionsStyleElement.innerHTML = styles;
this._selectionContainer.style.height = (<any>this._terminal)._viewportElement.style.height;
this._rowContainer.style.width = `${this.dimensions.canvasWidth}px`;
this._rowContainer.style.height = `${this.dimensions.canvasHeight}px`;
}
public setTheme(theme: ITheme | undefined): IColorSet {
if (theme) {
this.colorManager.setTheme(theme);
}
if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._themeStyleElement);
}
// Base CSS
let styles =
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this.colorManager.colors.foreground.css};` +
` background-color: ${this.colorManager.colors.background.css};` +
` font-family: ${this._terminal.getOption('fontFamily')};` +
` font-size: ${this._terminal.getOption('fontSize')}px;` +
`}`;
// Text styles
styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
` font-weight: ${this._terminal.options.fontWeight};` +
`}` +
`${this._terminalSelector} span.${BOLD_CLASS} {` +
` font-weight: ${this._terminal.options.fontWeightBold};` +
`}` +
`${this._terminalSelector} span.${ITALIC_CLASS} {` +
` font-style: italic;` +
`}`;
// Cursor
styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS} {` +
` background-color: ${this.colorManager.colors.cursor.css};` +
` color: ${this.colorManager.colors.cursorAccent.css};` +
`}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` +
` outline: 1px solid #fff;` +
` outline-offset: -1px;` +
`}`;
// Selection
styles +=
`${this._terminalSelector} .${SELECTION_CLASS} {` +
` position: absolute;` +
` top: 0;` +
` left: 0;` +
` z-index: 1;` +
` pointer-events: none;` +
`}` +
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
` position: absolute;` +
` background-color: ${this.colorManager.colors.selection.css};` +
`}`;
// Colors
this.colorManager.colors.ansi.forEach((c, i) => {
styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
});
this._themeStyleElement.innerHTML = styles;
return this.colorManager.colors;
}
public onWindowResize(devicePixelRatio: number): void {
this._updateDimensions();
}
private _refreshRowElements(cols: number, rows: number): void {
// Add missing elements
for (let i = this._rowElements.length; i <= rows; i++) {
const row = document.createElement('div');
this._rowContainer.appendChild(row);
this._rowElements.push(row);
}
// Remove excess elements
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop());
}
}
public onResize(cols: number, rows: number): void {
this._refreshRowElements(cols, rows);
this._updateDimensions();
}
public onCharSizeChanged(): void {
this._updateDimensions();
}
public onBlur(): void {
this._rowContainer.classList.remove(FOCUS_CLASS);
}
public onFocus(): void {
this._rowContainer.classList.add(FOCUS_CLASS);
}
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
// Remove all selections
while (this._selectionContainer.children.length) {
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
}
// Selection does not exist
if (!start || !end) {
return;
}
// Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.buffer.ydisp;
const viewportEndRow = end[1] - this._terminal.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1);
// No need to draw the selection
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) {
return;
}
// Create the selections
const documentFragment = document.createDocumentFragment();
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
if (columnSelectMode) {
documentFragment.appendChild(
this._createSelectionElement(viewportCappedStartRow, startCol, end[0], viewportCappedEndRow - viewportStartRow + 1)
);
} else {
// Draw first row
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount));
// Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
}
}
this._selectionContainer.appendChild(documentFragment);
}
/**
* Creates a selection element at the specified position.
* @param row The row of the selection.
* @param colStart The start column.
* @param colEnd The end columns.
*/
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
const element = document.createElement('div');
element.style.height = `${rowCount * this._terminal.charMeasure.height}px`;
element.style.top = `${row * this._terminal.charMeasure.height}px`;
element.style.left = `${colStart * this._terminal.charMeasure.width}px`;
element.style.width = `${this._terminal.charMeasure.width * (colEnd - colStart)}px`;
return element;
}
public onCursorMove(): void {
// No-op, the cursor is drawn when rows are drawn
}
public onOptionsChanged(): void {
// Force a refresh
this._updateDimensions();
this.setTheme(undefined);
this._terminal.refresh(0, this._terminal.rows - 1);
}
public clear(): void {
this._rowElements.forEach(e => e.innerHTML = '');
}
public refreshRows(start: number, end: number): void {
this._renderDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const terminal = this._terminal;
const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y;
const cursorX = this._terminal.buffer.x;
for (let y = start; y <= end; y++) {
const rowElement = this._rowElements[y];
rowElement.innerHTML = '';
const row = y + terminal.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row);
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorX, terminal.charMeasure.width, terminal.cols));
}
this._terminal.emit('refresh', {start, end});
}
private get _terminalSelector(): string {
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
}
}
@@ -0,0 +1,156 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory';
import { LineData } from '../../Types';
import { DEFAULT_ATTR } from '../../Buffer';
import { FLAGS } from '../Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
let rowFactory: DomRendererRowFactory;
let lineData: LineData;
beforeEach(() => {
dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document);
lineData = createEmptyLineData(2);
});
describe('createRow', () => {
it('should create an element for every character in the row', () => {
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span> </span>' +
'<span> </span>'
);
});
it('should set correct attributes for double width characters', () => {
lineData[0] = [DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)];
// There should be no element for the following "empty" cell
lineData[1] = [DEFAULT_ATTR, '', 0, undefined];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span style="width: 10px;">語</span>'
);
});
it('should add class for cursor', () => {
const fragment = rowFactory.createRow(lineData, true, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-cursor"> </span>' +
'<span> </span>'
);
});
it('should not render cells that go beyond the terminal\'s columns', () => {
lineData[0] = [DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)];
lineData[1] = [DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 1);
assert.equal(getFragmentHtml(fragment),
'<span>a</span>'
);
});
describe('attributes', () => {
it('should add class for bold', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.BOLD << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bold">a</span>' +
'<span> </span>'
);
});
it('should add class for italic', () => {
lineData[0] = [DEFAULT_ATTR | (FLAGS.ITALIC << 18), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-italic">a</span>' +
'<span> </span>'
);
});
it('should add classes for 256 foreground colors', () => {
const defaultAttrNoFgColor = (0 << 9) | (256 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoFgColor | (i << 9), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-fg-${i}">a</span>` +
'<span> </span>'
);
}
});
it('should add classes for 256 background colors', () => {
const defaultAttrNoBgColor = (257 << 9) | (0 << 0);
for (let i = 0; i < 256; i++) {
lineData[0] = [defaultAttrNoBgColor | (i << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bg-${i}">a</span>` +
'<span> </span>'
);
}
});
it('should correctly invert colors', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (2 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-2">a</span>' +
'<span> </span>'
);
});
it('should correctly invert default fg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (257 << 9) | (1 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-1 xterm-bg-15">a</span>' +
'<span> </span>'
);
});
it('should correctly invert default bg color', () => {
lineData[0] = [(FLAGS.INVERSE << 18) | (1 << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
'<span class="xterm-fg-0 xterm-bg-1">a</span>' +
'<span> </span>'
);
});
it('should turn bold fg text bright', () => {
for (let i = 0; i < 8; i++) {
lineData[0] = [(FLAGS.BOLD << 18) | (i << 9) | (256 << 0), 'a', 1, 'a'.charCodeAt(0)];
const fragment = rowFactory.createRow(lineData, false, 0, 5, 20);
assert.equal(getFragmentHtml(fragment),
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>` +
'<span> </span>'
);
}
});
});
});
function getFragmentHtml(fragment: DocumentFragment): string {
const element = dom.window.document.createElement('div');
element.appendChild(fragment);
return element.innerHTML;
}
function createEmptyLineData(cols: number): LineData {
const lineData: LineData = [];
for (let i = 0; i < cols; i++) {
lineData.push([DEFAULT_ATTR, ' ', 1, 32 /* ' '.charCodeAt(0) */]);
}
return lineData;
}
});
+90
View File
@@ -0,0 +1,90 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { LineData } from '../../Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../../Buffer';
import { FLAGS } from '../Types';
export const BOLD_CLASS = 'xterm-bold';
export const ITALIC_CLASS = 'xterm-italic';
export const CURSOR_CLASS = 'xterm-cursor';
export class DomRendererRowFactory {
constructor(
private _document: Document
) {
}
public createRow(lineData: LineData, isCursorRow: boolean, cursorX: number, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment();
let colCount = 0;
for (let x = 0; x < lineData.length; x++) {
// Don't allow any buffer to the right to be displayed
if (colCount >= cols) {
continue;
}
const charData = lineData[x];
const char: string = charData[CHAR_DATA_CHAR_INDEX];
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
const width: number = charData[CHAR_DATA_WIDTH_INDEX];
// The character to the left is a wide character, drawing is owned by the char at x-1
if (width === 0) {
continue;
}
const charElement = this._document.createElement('span');
if (width > 1) {
charElement.style.width = `${cellWidth * width}px`;
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
if (isCursorRow && x === cursorX) {
charElement.classList.add(CURSOR_CLASS);
}
// If inverse flag is on, the foreground should become the background.
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === 256) {
fg = 0;
}
if (bg === 257) {
bg = 15;
}
}
if (flags & FLAGS.BOLD) {
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
charElement.classList.add(BOLD_CLASS);
}
if (flags & FLAGS.ITALIC) {
charElement.classList.add(ITALIC_CLASS);
}
charElement.textContent = char;
if (fg !== 257) {
charElement.classList.add(`xterm-fg-${fg}`);
}
if (bg !== 256) {
charElement.classList.add(`xterm-bg-${bg}`);
}
fragment.appendChild(charElement);
colCount += width;
}
return fragment;
}
}
+3 -3
View File
@@ -6,7 +6,7 @@
import { FontWeight } from 'xterm';
import { CHAR_ATLAS_CELL_SPACING, ICharAtlasConfig } from './Types';
import { IColor } from '../Types';
import { isFirefox } from '../utils/Browser';
import { isFirefox, isSafari } from '../utils/Browser';
declare const Promise: any;
@@ -99,14 +99,14 @@ export function generateStaticCharAtlasTexture(context: Window, canvasFactory: (
// 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) {
if (!('createImageBitmap' in context) || isFirefox || isSafari) {
// 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;
}
// Transfer to an ImageBitmap is this is an OffscreenCanvas
return new Promise(r => r(canvas.transferToImageBitmap()));
return new Promise((r: (bitmap: ImageBitmap) => void) => r(canvas.transferToImageBitmap()));
}
const charAtlasImageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+1
View File
@@ -8,6 +8,7 @@ const userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform;
export const isFirefox = !!~userAgent.indexOf('Firefox');
export const isSafari = /^((?!chrome|android).)*safari/i.test(userAgent);
export const isMSIE = !!~userAgent.indexOf('MSIE') || !!~userAgent.indexOf('Trident');
// Find the users platform. We use this to interpret the meta key
+1 -1
View File
@@ -10,7 +10,7 @@ import { IDisposable } from 'xterm';
* @param type The event type.
* @param handler The handler for the listener.
*/
export function addDisposableListener(
export function addDisposableDomListener(
node: Element | Window | Document,
type: string,
handler: (e: any) => void,
@@ -5,6 +5,8 @@
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
import { Disposable } from '../common/Lifecycle';
import { addDisposableDomListener } from './Lifecycle';
const HOVER_DURATION = 500;
@@ -16,7 +18,7 @@ const HOVER_DURATION = 500;
* needed to support was single-line links which never overlap. Improvements can
* be made in the future.
*/
export class MouseZoneManager implements IMouseZoneManager {
export class MouseZoneManager extends Disposable implements IMouseZoneManager {
private _zones: IMouseZone[] = [];
private _areZonesActive: boolean = false;
@@ -30,13 +32,20 @@ export class MouseZoneManager implements IMouseZoneManager {
constructor(
private _terminal: ITerminal
) {
this._terminal.element.addEventListener('mousedown', e => this._onMouseDown(e));
super();
this.register(addDisposableDomListener(this._terminal.element, 'mousedown', e => this._onMouseDown(e)));
// These events are expensive, only listen to it when mouse zones are active
this._mouseMoveListener = e => this._onMouseMove(e);
this._clickListener = e => this._onClick(e);
}
public dispose(): void {
super.dispose();
this._deactivate();
}
public add(zone: IMouseZone): void {
this._zones.push(zone);
if (this._zones.length === 1) {
@@ -3,6 +3,8 @@
* @license MIT
*/
import { Disposable } from '../common/Lifecycle';
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
/**
@@ -15,7 +17,7 @@ export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRat
* The listener should fire on both window zoom changes and switching to a
* monitor with a different DPI.
*/
export class ScreenDprMonitor {
export class ScreenDprMonitor extends Disposable {
private _currentDevicePixelRatio: number;
private _outerListener: MediaQueryListListener;
private _listener: ScreenDprListener;
@@ -33,6 +35,11 @@ export class ScreenDprMonitor {
this._updateDpr();
}
public dispose(): void {
super.dispose();
this.clearListener();
}
private _updateDpr(): void {
// Clear listeners for old DPR
if (this._resolutionMediaMatchList) {
+3 -1
View File
@@ -3,7 +3,9 @@
* @license MIT
*/
export interface IMouseZoneManager {
import { IDisposable } from 'xterm';
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
}
+1 -1
View File
@@ -39,7 +39,7 @@ describe('clone', () => {
});
it('should clone null values', () => {
const test = {
const test: any = {
a: null
};
+11 -4
View File
@@ -107,7 +107,6 @@ export class MockTerminal implements ITerminal {
children: HTMLElement[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
scrollback: number;
buffers: IBufferSet;
buffer: IBuffer;
@@ -182,9 +181,8 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCurAttr: number;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
@@ -268,7 +266,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
throw new Error('Method not implemented.');
}
setOption(key: string, value: any): void {
this.options[key] = value;
(<any>this.options)[key] = value;
}
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
@@ -303,6 +301,9 @@ export class MockBuffer implements IBuffer {
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
}
getWrappedRangeForLine(y: number): { first: number; last: number; } {
throw new Error('Method not implemented.');
}
nextStop(x?: number): number {
throw new Error('Method not implemented.');
}
@@ -315,6 +316,9 @@ export class MockBuffer implements IBuffer {
}
export class MockRenderer implements IRenderer {
dispose(): void {
throw new Error('Method not implemented.');
}
colorManager: IColorManager;
on(type: string, listener: XtermListener): void {
throw new Error('Method not implemented.');
@@ -343,6 +347,9 @@ export class MockRenderer implements IRenderer {
}
export class MockViewport implements IViewport {
dispose(): void {
throw new Error('Method not implemented.');
}
scrollBarWidth: number = 0;
onThemeChanged(colors: IColorSet): void {
throw new Error('Method not implemented.');
+5
View File
@@ -139,6 +139,11 @@
cursor: pointer;
}
.xterm.xterm-cursor-crosshair {
/* Column selection mode */
cursor: crosshair;
}
.xterm .xterm-accessibility,
.xterm .xterm-message {
position: absolute;
+1 -1
View File
@@ -5,6 +5,6 @@
* This file is the entry point for browserify.
*/
import { Terminal } from './Terminal';
import { Terminal } from './public/Terminal';
module.exports = Terminal;
+9 -6
View File
@@ -3,19 +3,22 @@
"module": "commonjs",
"target": "es5",
"lib": [
"DOM",
"ES5",
"ScriptHost",
"ES2015.Promise"
"dom",
"es5",
"scripthost",
"es2015.promise"
],
"rootDir": "src",
"outDir": "lib",
"sourceMap": true,
"removeComments": true,
"noUnusedLocals": true
"preserveWatchOutput": true,
"noUnusedLocals": true,
"noImplicitAny": true
},
"include": [
"src/**/*"
"src/**/*",
"typings/xterm.d.ts"
],
"exclude": [
"src/addons/**/*"
+13 -2
View File
@@ -31,6 +31,7 @@
"parameter"
],
"eofline": true,
"new-parens": true,
"no-duplicate-imports": true,
"no-eval": true,
"no-internal-module": true,
@@ -93,13 +94,23 @@
"naming-convention": [
true,
{"type": "property", "modifiers": ["public", "static", "const"], "format": "UPPER_CASE"}
{"type": "default", "format": "camelCase", "leadingUnderscore": "forbid"},
{"type": "type", "format": "PascalCase"},
{"type": "class", "format": "PascalCase"},
{"type": "property", "modifiers": ["const"], "format": "UPPER_CASE"},
{"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "allow"},
// TODO: Change allow to require when there aren't many PRs out
// {"type": "member", "modifiers": ["protected"], "format": "camelCase", "leadingUnderscore": "require"},
{"type": "member", "modifiers": ["private"], "format": "camelCase", "leadingUnderscore": "require"},
{"type": "variable", "modifiers": ["const"], "format": ["camelCase", "UPPER_CASE"]},
{"type": "interface", "prefix": "I"}
],
"no-else-after-return": {
"options": "allow-else-if"
},
"prefer-const-enum": [
true
]
],
"prefer-const": true
}
}
+32 -1
View File
@@ -13,6 +13,11 @@ declare module 'xterm' {
*/
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900';
/**
* A string representing a renderer type.
*/
export type RendererType = 'dom' | 'canvas';
/**
* An object containing start up options for the terminal.
*/
@@ -119,6 +124,32 @@ declare module 'xterm' {
*/
macOptionIsMeta?: boolean;
/**
* Whether holding a modifier key will force normal selection behavior,
* regardless of whether the terminal is in mouse events mode. This will
* also prevent mouse events from being emitted by the terminal. For example,
* this allows you to use xterm.js' regular selection inside tmux with
* mouse mode enabled.
*/
macOptionClickForcesSelection?: boolean;
/**
* (EXPERIMENTAL) The type of renderer to use, this allows using the
* fallback DOM renderer when canvas is too slow for the environment. The
* following features do not work when the DOM renderer is used:
*
* - Links
* - Line height
* - Letter spacing
* - Cursor blink
* - Cursor style
*
* This option is marked as experiemental because it will eventually be
* moved to an addon. You can only set this option in the constructor (not
* setOption).
*/
rendererType?: RendererType;
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
@@ -530,7 +561,7 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'termName'): string;
getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold'| 'rendererType' | 'termName'): string;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.