Compare commits

...
590 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
Paris Kasidiaris 0ad1080133 Bump version to 3.4.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-05-19 08:31:27 +00:00
Daniel Imms 1736c1e12d Prevent outside line-height styles from affecting cursor
See Microsoft/vscode#50125
2018-05-18 12:17:39 -07:00
Benjamin Woodruff 4717619f76 Merge pull request #1327 from bgw/dynamic-char-atlas
Modularize the character atlas system, add a LRU-cache based dynamic character atlas
2018-05-17 22:59:03 -07:00
Benjamin Woodruff 7f761b5b9e Merge branch 'master' into dynamic-char-atlas
I did a bunch of testing of various ansi escape codes (including
italics) with all three char atlas implementations (none, static,
dynamic), with and without `drawBoldTextInBrightColors`, and with and
without `enableBold` to test all of the edge cases that I could.
2018-05-17 22:42:52 -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 7334e6ab19 Merge pull request #1452 from Tyriar/coveralls
Update coveralls
2018-05-15 11:03:22 -07:00
Daniel Imms 137b499538 Merge pull request #1450 from Tyriar/tagline
Update tagline
2018-05-15 11:03:08 -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
Daniel Imms 3acd1eaecc Update coveralls
This broke after we moved the organization

Fixes #1451
2018-05-14 11:48:13 -07:00
Daniel Imms 435cf91ec7 Merge pull request #1433 from Tyriar/fix_lint
Run tslint on all of src/ (for good this time)
2018-05-14 11:33:30 -07:00
Daniel Imms 6e451f58b8 Update tagline 2018-05-14 11:13:01 -07:00
Daniel Imms 7a9f291df9 Merge pull request #1446 from Tyriar/1437_fix_link_pointer
Fix link cursor CSS
2018-05-12 07:45:20 -07:00
Daniel Imms 9dfce63e55 Simplify cursor styles 2018-05-12 07:44:30 -07:00
Daniel Imms 139a246436 Merge pull request #1401 from npezza93/alt-click-map-bug
Handle if getCoords returns null in the AltClickHandler
2018-05-11 15:56:14 -07:00
Daniel Imms 343cc2b7b2 Merge branch 'master' into alt-click-map-bug 2018-05-11 15:50:40 -07:00
Daniel Imms 0375e0064b Fix link cursor CSS
Ensure pointer overrides the default mouse events mode cursor.

Fixes #1437
2018-05-11 10:03:43 -07:00
Daniel Imms 57e35f836e Merge pull request #1438 from diag/master
Add diag.ai to list of real-world uses
2018-05-11 09:16:30 -07:00
Daniel Imms 685757b04b Merge pull request #1417 from pro-src/packaging
Stop publishing test files to npm - closes #1361
2018-05-11 07:51:46 -07:00
Daniel Imms 210a312fbf Remove ignore key from package.json 2018-05-11 07:49:37 -07:00
Daniel Imms c366ee20b8 Merge branch 'master' into packaging 2018-05-11 07:48:34 -07:00
Daniel Imms b9baacac3a Merge pull request #1444 from pengx17/patch-1
Update how to use addons with Typescript
2018-05-11 07:20:17 -07:00
Peng Xiao 45fea0e790 Merge branch 'master' into patch-1 2018-05-11 21:57:40 +08:00
Benjamin Woodruff cded6f80f6 Merge pull request #1445 from pro-src/master
Fix tsc
2018-05-10 23:57:24 -07:00
pro-src 9c1bee300d Fix tsc 2018-05-11 01:51:49 -05:00
Peng Xiao ef4cda2cc6 revise importing addons in ts 2018-05-11 11:20:02 +08:00
Peng Xiao d315dd6684 Merge branch 'master' into patch-1 2018-05-11 11:12:02 +08:00
Benjamin Woodruff 8a5891ede2 Merge pull request #1442 from bgw/dim-save-restore
Save and restore the ctx for cached characters
2018-05-10 09:38:46 -07:00
Peng Xiao bce81f304e Update how to use addons with Typescript 2018-05-10 16:11:25 +08:00
Benjamin Woodruff 3971fc9b4e Save and restore the ctx for cached characters
Drawing dim colors sets the globalAlpha to 0.5, so we need to
save/restore the ctx state. Otherwise, every time we draw some dim
content, our display will get progressively dimmer.

I tested this by running `echo '\u001b[2mfoo'` a few times in zsh.

Fixes #1424
2018-05-09 20:34:55 -07:00
npezza93 4edbd507e5 Check if endCol and endRow are present before handling an alt click 2018-05-09 19:35:02 -04:00
npezza93 b162582e66 Handle if getCoords returns null in the AltClickHandler
Fixes #1397
2018-05-09 19:31:19 -04:00
Benjamin Woodruff be32695e16 Merge pull request #1391 from LinusU/bright-vs-bold
Separate bright and bold
2018-05-08 18:43:41 -07:00
Benjamin Woodruff 9893338020 Add drawBoldTextInBrightColors to DEFAULT_OPTIONS
This lets term.setOption() work for drawBoldTextInBrightColors.
2018-05-08 18:34:23 -07:00
Ledion Bitincka cfdec9ff90 add diag.ai 2018-05-08 16:27:54 -07:00
Jörg Breitbart 02893dcbb8 fix some docs 2018-05-07 23:28:40 +02:00
Linus Unnebäck ce6139e604 Add drawBoldTextInBrightColors option 2018-05-07 22:23:04 +02:00
Linus Unnebäck a23354f018 Fully use char atlas for bright & bold colors 2018-05-07 22:23:01 +02:00
Linus Unnebäck 41fd563144 Separate bright and bold 2018-05-07 22:19:01 +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 9bcecd4e2e Run tslint on all of source (for good this time) 2018-05-04 10:20:09 -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
Daniel Imms e48b15e859 Merge pull request #1422 from flybayer/italic
Add support for italic rendering
2018-05-01 11:56:21 -07:00
Jörg Breitbart 89cf45c514 save func pointer in local var 2018-05-01 20:51:36 +02:00
Daniel Imms a732952677 Rearrange italic/bold to get _ctx.font validating 2018-05-01 11:50:06 -07: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
Daniel Imms b60ab119b2 Merge branch 'master' into italic 2018-05-01 11:30:03 -07: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
Benjamin Woodruff 381d3e1444 Merge pull request #1413 from bgw/batch-background
Batch background draws together to reduce draws
2018-04-27 20:53:32 -07:00
Benjamin Woodruff b97eaf2c52 Merge branch 'master' into batch-background 2018-04-27 20:49:03 -07:00
Benjamin Woodruff 513847fd80 Make BaseRenderLayer's _alpha private again
This was accidentally left over from some earlier changes I was playing
with.
2018-04-27 20:47:55 -07:00
Daniel Imms 7b2500bfd6 Merge pull request #1420 from pro-src/patch-1
Update README.md
2018-04-27 09:44:47 -07:00
pro-src 882d5a86d3 Update README.md - prefer hex escapes over octal 2018-04-27 02:19:48 -05:00
Brandon Bayer 235982362c Add support for italic rendering 2018-04-26 17:34:01 -04:00
Daniel Imms aaaec75d73 Add remaining naming conventions into tslint 2018-04-26 13:54:20 -07:00
pro-src 30b88113a8 explicitly exclude demo 2018-04-26 12:09:21 -05:00
pro-src 994895af6d Update README.md
Replace the deprecated octal escapes with Unicode escapes.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Deprecated_octal
2018-04-26 11:53:20 -05:00
pro-src a61ba2e140 closes #1361 2018-04-26 09:18:18 -05:00
Benjamin Woodruff a5a03c40f2 Batch background draws together to reduce draws
Adjacent cells on the same row sharing the same background color will be
drawn using the same `fillCells` call. This should make drawing an
application with a solid or mostly solid background color (e.g. vim)
faster.

I set vim to draw the background color for a file, and started scrolling
through it. Before, _drawBackground was taking around 6ms per frame.
After this commit, it was taking around 0.5ms per frame.

Based on prior experience, I'd expect these results to be more drastic
for larger terminal windows. The demo's window is pretty small.
2018-04-24 23:52:13 -07:00
Daniel Imms 83fe87d012 Merge pull request #1407 from Tyriar/enum_names
Enforce upper case for public const/enum members and const
2018-04-24 11:00:58 -07:00
Daniel Imms 3713d282d8 Fix conflicts 2018-04-24 10:55:53 -07:00
Daniel Imms 8aa65d06f5 Merge pull request #1404 from Tyriar/no_else_return
Add no else return tslint rule
2018-04-24 10:50:27 -07:00
Daniel Imms fd97fead60 Merge branch 'master' into no_else_return 2018-04-24 10:44:54 -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
Benjamin Woodruff 453da35f51 Merge branch 'master' into dynamic-char-atlas 2018-04-23 22:43:43 -07:00
Benjamin Woodruff d254d69ee2 Fix fontWeight options with DynamicCharAtlas
The atlas wasn't using these values when setting the font.
2018-04-23 22:15:45 -07:00
Benjamin Woodruff ff19ba8e33 Merge pull request #1393 from bgw/text-render-layer
Separate foreground & background rendering passes
2018-04-23 21:48:09 -07:00
Daniel Imms fcde7be62a Enforce const enums
Fixes #1408
2018-04-23 17:35:41 -07:00
Daniel Imms 45b4429712 Merge branch 'master' into enum_names 2018-04-23 17:18:40 -07:00
Daniel Imms 9942dc384d Enforce upper case for public const/enum members
Fixes #1406
2018-04-23 17:17:21 -07: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
Daniel Imms 56f9d423fe Merge branch 'master' into text-render-layer 2018-04-23 14:15:45 -07:00
Daniel Imms 716a8d5926 Merge pull request #1403 from jerch/performance_optimizations
performance optimizations
2018-04-23 13:49:03 -07:00
Jörg Breitbart 9593cb0cc8 readonly lines; fix for MockBuffer 2018-04-23 22:40:24 +02:00
Jörg Breitbart 7ed7505ce4 change _lines to lines in Buffer 2018-04-23 22:18:05 +02:00
Jörg Breitbart 6ff4ae7194 revert to local const buffer 2018-04-23 22:06:24 +02:00
Daniel Imms 9e863d0377 Add no else return tslint rule 2018-04-23 11:52:42 -07:00
Jörg Breitbart 13d4ab6f8d remove remnant 2018-04-23 18:32:32 +02:00
Jörg Breitbart 0d7ecd9129 prefetch row in addChar 2018-04-23 18:26:06 +02:00
Jörg Breitbart dd096c2e8b const buffer 2018-04-23 17:19:42 +02:00
Jörg Breitbart 0decd77253 map Buffer.lines directly to Buffer._lines 2018-04-23 16:59:55 +02:00
Jörg Breitbart 2bfe74490a make .buffer local where appropriate 2018-04-23 16:29:19 +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
Benjamin Woodruff 0fe59df2e9 Rename startRow/endRow in TextRenderLayer
Renaming these to firstRow/lastRow makes the inclusivity of the range
clearer.

Addresses this comment:
https://github.com/xtermjs/xterm.js/pull/1393#discussion_r182471582
2018-04-18 20:22:55 -07:00
Daniel Imms a3ce881897 Merge pull request #1372 from Tyriar/1341_memory_leak
Remove terminal references on dispose
2018-04-18 08:10:20 -07:00
Daniel Imms 58c50f08db Break when the terminal reference is removed 2018-04-18 08:10:05 -07:00
Daniel Imms 984e027c87 Merge branch 'master' into 1341_memory_leak 2018-04-18 08:02:55 -07:00
Benjamin Woodruff b853aa4929 Separate foreground & background rendering passes
I'm primarily interested in doing this because it'll allow me to
optimize the background rendering in later diffs, but this may also make
it possible to fix some minor rendering issues.

Right now, if we draw a single-width character that overflows its cell's
bounds, and the character to the right of it has a background, that
background may cover up the first character's foreground. By drawing the
background in a separate pass, we can avoid those cases.

There's still some issues with how dirty regions are computed that makes
rendering stuff like that flaky, but this at least gets us closer to
"correct" rendering.

Other terminal emulators (e.g. alacritty) render the foreground and
background in separate passes:
https://github.com/jwilm/alacritty/blob/1b7ffea/src/renderer/mod.rs#L766
2018-04-17 21:44:17 -07:00
Benjamin Woodruff 4fd630ec5c Add code to IGlyphIdentifer, use it
It turns out that `glyph.char.charCodeAt(0)` isn't equivalent to the
character's actual code because of utf-16 garbage, so this adds
`glyph.code` to the `glyph` object so that we can use the `code` value,
and don't need to regenerate it.
2018-04-16 09:49:32 -07:00
Benjamin Woodruff ec96c40fde Re-add colors.ansi slicing in generateConfig
- We need to make a clone of this array, because ColorManager mutates
  it.
- We only want to compare the first 16 colors instead of all 256, since
  only the first 16 will ever change. In DynamicCharAtlas, we can pull
  the additional colors from DEFAULT_ANSI_COLORS.

I tested this out by loading the demo and changing the theme a few
times.
2018-04-16 09:41:00 -07:00
Daniel Imms cf8fa10205 Merge pull request #1299 from jonaustin/patch-1
Add Hyper to list of apps
2018-04-16 09:40:07 -07:00
Daniel Imms d8d9d904b5 Merge branch 'master' into patch-1 2018-04-16 09:39:02 -07:00
Benjamin Woodruff 4a13813941 Rename DRAW_TO_CACHE_LIMIT to FRAME_CACHE_DRAW_LIMIT 2018-04-16 08:56:54 -07:00
Daniel Imms cd7911a3d4 Merge pull request #1390 from Tyriar/third_party_addons
Add xterm-webfont addon to readme
2018-04-16 06:55:35 -07:00
Daniel Imms 3625a2278d Merge pull request #1365 from Tyriar/1364_addon_lint
Lint addon files
2018-04-16 06:46:42 -07:00
Daniel Imms 7d108999ff Fix lint 2018-04-16 06:42:16 -07:00
Daniel Imms e3dc36c123 Add xterm-webfont addon to readme 2018-04-16 06:28:45 -07:00
Daniel Imms 11541be4ae Merge branch 'master' into 1364_addon_lint 2018-04-16 06:25:48 -07:00
Daniel Imms ceffb35445 Merge pull request #1369 from Tyriar/897_dead_options
Remove unused options
2018-04-16 06:25:18 -07:00
Daniel Imms e1957ae6aa Merge remote-tracking branch 'origin/master' into 1364_addon_lint 2018-04-16 06:24:37 -07:00
Benjamin Woodruff 5a05db727c Add a per-frame limit on dynamic atlas writes
This limits the amount of damage a pathological program can cause in
thrashing the LRU cache.

I ran the script described here:
https://github.com/xtermjs/xterm.js/pull/1327#issuecomment-374003960

Without this patch, the entire output would get drawn in one frame that
took about 140 ms to draw. With this patch, it takes about 60 ms. It's
still nowhere as good as the static or none atlas implementations, but
it's not terrible, like it was.

This should have minimal impact on real-world applications: it'll just
take a little longer for the atlas to warm up.

The implementation is a little hacky, since it involves throwing some
code into TextRenderLayer that violates the separation of concerns, but
we can clean this up later.
2018-04-15 20:02:39 -07:00
Benjamin Woodruff 1f692ed047 Use a single tmpCtx, avoid parsing color values
Pros:
- The code is simpler.
- AA rendering is consistent between cached and uncached characters.
- No long requires parsing color values to determine the alpha, which
  means that we no longer depend on #rrggbbaa color value support.

Cons:
- We're drawing without SPAA in some cases where we could be.

Addresses this comment:
https://github.com/xtermjs/xterm.js/pull/1327#discussion_r176109332
2018-04-15 19:59:36 -07:00
Benjamin Woodruff c3eea02855 Add a LRUMap test, fix a bug in LRUMap
Bug: When appending a node, we weren't setting the tail's `next` value
correctly.
2018-04-15 19:48:37 -07:00
Benjamin Woodruff c6d66d7b0d Fix subpixel antialising in DynamicCharAtlas
Drawing to a ctx with `{alpha: true}` causes the canvas API (on chrome
at least) to switch to grayscale AA, instead of RGB subpixel AA.

This fixes it by drawing to a ctx that's using `{alpha: false}` when
posisble, and switching to a ctx that's using `{alpha: true}` only when
we have to.
2018-04-15 19:48:37 -07:00
Benjamin Woodruff 9c0ce1d742 Fix DynamicCharAtlas with transparency
There were two bugs:
- We need to clear the background of _tmpCtx instead of just drawing on
  top if the background color is transparent.
- We should set the background to fully transparent if it's partially
  transparent, to avoid drawing the transparent background twice.
2018-04-15 19:43:23 -07:00
Benjamin Woodruff ac7db87fd0 Remove use of Promises in {Base,Static}CharAtlas
Nothing actually cares about the return value, so we can avoid depending
on `Promise` by not returning a promise.

This should make this work for IE11 (though I haven't tested that).

I tried this in Chrome and Firefox to test both codepaths (ImageBitmap
and HTMLCanvasElement).
2018-04-15 18:52:06 -07:00
Benjamin Woodruff 8b3b7535c6 Add a new LRUMap class, remove uses of Map
LRUMap implements the parts of an ordered map that we need to
efficiently implement DynamicCharAtlas. It's more code, but there's a
few advantages of this approach:

- Map isn't available on some older browsers, so this removes the need
  for a polyfill.

- Moving an item to the end of the map's iteration order now only
  requires unlinking and linking a linked-list node, whereas before we
  had to delete and re-insert our value.

- Peeking at the oldest entry in the map no longer requires allocating
  and destroying an iterator.

- We can preallocate the linked-list nodes we want to improve cache
  locality. Similarly, we can recycle linked-list nodes to reduce
  allocations and the GC pauses those allocations may cause.

- LRUMap seems to give slightly better results in Chrome's profiler than
  Map did. We now spend about 5% of our time on map operations instead
  of about 10%.

- In my (limited) testing, it doesn't look like LRUMap is slowing down
  over time. Map appeared to get slightly slower the longer I ran the
  terminal for, either due to memory fragmentation or some sort of leak.

I still need to write some tests for LRUMap, but I've been using this
implementation for the last hour without problems.
2018-04-15 18:52:06 -07:00
Benjamin Woodruff c8b500c182 Skip drawing whitespace from DynamicCharAtlas
When we remove the background from a glyph, we can keep track of if we
made all the pixels fully transparent. If we did, we can just skip
future calls to _drawFromCache, since it would be a noop anyways.
2018-04-15 18:52:06 -07:00
Benjamin Woodruff 24feaabd9a Add a basic dynamic character atlas implementation
This adds a very minimal implementation of DynamicCharAtlas using a LRU
cache.

I've got some optimizations that I'll add on top of this, but this
proves the concept.
2018-04-15 18:49:07 -07:00
Benjamin Woodruff 8c980bc30d Add a dummy NoneCharAtlas implementation
This adds the logic needed to switch between multiple char atlas
implementations, and an example second implementation to test it out
with.

This also offers an escape hatch if something goes wrong with the char
atlas implementation(s).
2018-04-15 18:03:55 -07:00
Benjamin Woodruff 6f21385cd8 Add BaseCharAtlas and implement StaticCharAtlas
The API defined by BaseCharAtlas will let us support multiple char atlas
implementations at once.

I tested this by running the demo with Chrome's profiler, which shows
that calls are going to StaticCharAtlas/drawImage instead of
_drawUncachedChar. Applications using 256 colors also still work fine,
via _drawUncachedChar.
2018-04-15 18:03:55 -07:00
Daniel Imms 83f9d59d91 Merge pull request #1384 from Tyriar/1383_setoption_exception
Guard usages of DOM attached vars in setOption
2018-04-15 13:30:33 -07: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
Daniel Imms 2cee66909b Merge pull request #1374 from Tyriar/1353_run_lint_with_tests
Run lint with tests
2018-04-13 06:20:11 -07:00
Daniel Imms da8151b1b5 Merge pull request #1375 from Tyriar/remove-package-lock
Remove package-lock.json
2018-04-12 10:03:44 -07:00
Daniel Imms a3cb34fbf1 Merge pull request #1385 from johnpoth/headless-mode
Check if charMeasure is defined in headless mode
2018-04-12 07:29:48 -07:00
jpoth 9a1162d7f2 Check if charMeasure is defined in headless mode 2018-04-12 15:43:09 +02:00
Daniel Imms 591b09a62b Merge pull request #1377 from Tyriar/remove_jsdoc
Remove docdash
2018-04-11 09:39:12 -07:00
Daniel Imms f6f3e78e2c Merge pull request #1376 from Tyriar/1324_move_zmodem_demo
Move zmodem demo to demo/
2018-04-11 09:38:56 -07:00
Paris Kasidiaris 048b10ebf6 Bump version to 3.3.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-04-11 16:19:48 +00:00
Daniel Imms c882dcc353 Merge pull request #1381 from Tyriar/1379_viewport_throw
Fix exception when reset is called before open
2018-04-11 07:20:48 -07:00
Daniel Imms 82897ed56c Guard usages of DOM attached vars in setOption
Fixes #1383
2018-04-10 19:54:49 -07:00
Daniel Imms cd06786d16 Fix exception when reset is called before open
Fixes #1379
2018-04-09 06:13:32 -07:00
Daniel Imms 74d91a6bdd Remove docdash 2018-04-07 11:42:36 -07:00
Daniel Imms 4a1ebbfb56 Add .npmrc 2018-04-07 11:40:54 -07:00
Felipe Gasper 86c9b3f92c Fix zmodem demo. 2018-04-05 07:03:09 -07:00
Daniel Imms 4fbd1adaa9 Move zmodem demo to demo/
Fixes #1324
2018-04-04 14:31:25 -07:00
Daniel Imms 004ea61b83 Remove package-lock.json
We don't have any dependencies so this isn't as important to us, it's also ugly
to keep seeing these in PRs.
2018-04-04 14:11:46 -07:00
Daniel Imms 9be57f72f0 Run lint with tests
Fixes #1353
2018-04-04 14:10:03 -07:00
Daniel Imms e5ee76ded4 Remove terminal references on dispose
Part of #1341
Fixes #1370
2018-04-04 11:51:50 -07:00
Daniel Imms 87f21f14a1 Remove unused options
Fixes #897
2018-04-04 11:20:25 -07:00
Daniel Imms ef16811268 Merge branch 'master' into 1364_addon_lint 2018-04-04 11:11:05 -07:00
Daniel Imms 989f5873f5 Merge pull request #1366 from Tyriar/1363_fix_demo
Prevent falsy write values
2018-04-02 08:23:53 -07:00
Daniel Imms 307744be66 Prevent falsy write values
Fixes #1363
2018-04-02 08:22:22 -07:00
Daniel Imms 768539a57c Fix lint errors in attach 2018-04-02 08:17:44 -07:00
Daniel Imms 22c4197a1d Lint addons 2018-04-02 08:16:39 -07:00
Daniel Imms d4b75e4909 Merge pull request #1346 from bgw/color-parsing
Allow theming with arbitrary CSS color strings
2018-03-30 07:57:19 -07:00
Daniel Imms 4b5de031ea Merge branch 'master' into color-parsing 2018-03-30 07:50:39 -07:00
Benjamin Woodruff b66b0aae18 Block transparent colors without allowTransparency
Fixes xterm/xterm.js#1357.
2018-03-29 22:04:24 -07:00
Paris Kasidiaris 36a1c5b922 Merge pull request #1355 from AlexandrePetit-Pas/patch-5
Handle blob in attach Addon
2018-03-28 11:09:29 +03:00
Paris Kasidiaris 63f711e634 Fix styling and make string messages to work again 2018-03-28 10:57:21 +03:00
Alexandre Petit-Pas 516903e29e Handle blob in attach Addon
I have added blob handler for the attach Addon.
This allows to handle Docker with ws (works on Docker API v1.35)
I have also added a callback to handle asynchronous FileReader.
2018-03-28 07:37:02 +00:00
Paris Kasidiaris 5cf9423699 Merge pull request #1356 from xtermjs/fix-linter
Make linter blissful again
2018-03-28 10:31:29 +03:00
Paris Kasidiaris dbb2320e83 Fix linting errors 2018-03-28 10:22:42 +03:00
Benjamin Woodruff 0cb6138cd0 Allow theming with arbitrary CSS color strings
This mostly worked before, but clearColor made the assumption that the
colors were always in #RRBBGG format. See the discussion here:
https://github.com/xtermjs/xterm.js/pull/1327#issuecomment-374812842

This changes the internal representation of a color so that we hold onto
into an object containing the original css string along with an RGBA
value encoded as a 32-bit uint. clearColor can then use that RGBA
representation.

Additionally, this deduplicates some code between ColorManager and
Terminal. Terminal was generating the 256 ansi colors the same way
ColorManager was, so this just makes Terminal use the same constant.
2018-03-26 09:47:25 -07:00
Daniel Imms bf21097430 Merge pull request #1326 from LucianBuzzo/1322-options-clone
Do not mutate options object used in constructor
2018-03-24 14:07:59 -07:00
Lucian Buzzo 55d6daf8ee Do not mutate options object used in constructor
Connects to #1322

change-type: patch
2018-03-24 16:25:02 +00:00
Daniel Imms 6e1e63e666 Merge pull request #1350 from FGasper/issue_1330_rtl_layout
Force LTR for the terminal element
2018-03-23 16:19:02 -07:00
Daniel Imms b27d5bb08d Use single quote to make tslint happy 2018-03-23 16:18:41 -07:00
Felipe Gasper e63831febc Force LTR for the terminal element
Issue #1330: This prevents RTL layouts from mishandlin the
intended layout of the terminal container and children.
2018-03-23 19:16:02 -04:00
Daniel Imms 4ab79c908a Merge pull request #1329 from Tyriar/1319_bang_operator
Use bang operator for array methods
2018-03-21 07:34:18 -07:00
Daniel Imms b85083c3e8 Merge pull request #1303 from Tyriar/24_multi_line_links
Get multi-line links working
2018-03-21 07:33:43 -07:00
Daniel Imms 97f592ce8b Merge remote-tracking branch 'origin/master' into 24_multi_line_links 2018-03-21 07:32:32 -07:00
Daniel Imms 70da707569 Merge pull request #1338 from Tyriar/1325_marker_api
Implement Markers API
2018-03-20 19:14:10 -07:00
Daniel Imms 63f24be2b9 Merge branch 'master' into 1325_marker_api 2018-03-20 19:02:52 -07:00
Daniel Imms 0d6be9e03e Merge pull request #1344 from Tyriar/trailing_comma
Disallow trailing commas
2018-03-20 19:01:04 -07:00
Daniel Imms 3cbb21a966 Merge pull request #1340 from Tyriar/unused_func
Remove unused function
2018-03-20 19:00:35 -07:00
Daniel Imms 1fd7348938 Merge branch 'master' into unused_func 2018-03-20 19:00:24 -07:00
Daniel Imms ae32893732 Disallow trailing commas 2018-03-20 09:10:48 -07:00
Daniel Imms 4f82cf26f0 Merge pull request #1336 from mareklibra/removeStyle
Replace inline-styles by external CSS
2018-03-20 09:07:06 -07:00
Marek Libra b67b65cd1e Replace inline-styles by external CSS
Selected inline-styles were externalized to xterm.css.

The `Terminal._charSizeStyleElement` is removed since it
was not used anymore.

Fixes: https://github.com/xtermjs/xterm.js/issues/1335
2018-03-20 13:45:08 +01:00
Daniel Imms 0eeec13653 Clean up 2018-03-19 11:01:26 -07:00
Daniel Imms 5d1f744282 Remove unused function 2018-03-19 08:20:28 -07:00
Daniel Imms 387d5b056d Add Terminal.scrollToLine tests 2018-03-17 11:46:37 -07:00
Daniel Imms cbf7419601 Flag markers and addMarker as experimental 2018-03-16 17:24:04 -07:00
Daniel Imms 5c1a4a7d01 Add tests for Buffer.addMarker 2018-03-16 17:23:17 -07:00
Daniel Imms cbc6d7ce32 Add selectLines test 2018-03-16 15:56:48 -07:00
Daniel Imms b80832ac4c Initial marker API implementation
Part of #1325
2018-03-16 12:53:55 -07:00
Daniel Imms 3716aad202 Backtrack row to be linkified if the first row is wrapped 2018-03-16 07:21:14 -07:00
Daniel Imms efbd542964 Merge branch 'master' into 24_multi_line_links 2018-03-16 07:14:20 -07:00
Daniel Imms 888070830d Merge pull request #1332 from xtermjs/Tyriar-patch-1
Add note about third party dependencies
2018-03-13 12:44:51 -07:00
Daniel Imms 9905e2aa1d Add note about third party dependencies
Fixes #1328
2018-03-13 11:19:51 -07:00
Daniel Imms a59af6606b Use bang operator for array methods
Part of #1319
2018-03-12 14:14:37 -07:00
Daniel Imms d1213e5656 Merge pull request #1320 from Tyriar/1319/AccessibilityManager
Fix strictNullChecks errors in AccessibilityManager
2018-03-12 08:51:32 -07:00
Daniel Imms 0e14798d69 Merge pull request #1321 from felixse/patch-1
Add Fluent Terminal to the list of real-world uses
2018-03-11 13:44:16 -07:00
Daniel Imms c8ab687fa0 Add tests 2018-03-11 12:34:27 -07:00
Daniel Imms e5f5c2baf2 Fix links that start on wrapped lines 2018-03-10 16:08:50 -08:00
Felix 2f861602c8 Add Fluent Terminal to the list of real-world uses 2018-03-10 23:27:59 +01:00
Daniel Imms 4f0cc01485 Fix strictNullChecks errors in AccessibilityManager
Part of #1319
2018-03-09 11:27:10 -08:00
Daniel Imms dd41ab38bb Fix imports after noUnusedLocals was merged in 2018-03-09 10:58:57 -08:00
Daniel Imms 255cb5de3a Merge branch 'master' into 24_multi_line_links 2018-03-09 10:57:04 -08:00
Daniel Imms b44a11c42e Remove TODOs, polish 2018-03-09 10:56:54 -08:00
Daniel Imms 90b2f0e6a1 Merge pull request #1316 from Tyriar/noUnusedLocals
Add noUnusedLocals tsconfig flag, fix issues
2018-03-09 08:43:40 -08:00
Daniel Imms 811aa9c413 Merge remote-tracking branch 'origin/master' into noUnusedLocals 2018-03-09 08:43:21 -08:00
Daniel Imms 842c7132ec Merge pull request #1318 from Tyriar/remove_unused_css
Remove unused CSS rule
2018-03-09 08:41:41 -08:00
Daniel Imms efb67edc36 Remove unused CSS rule
With #1316, this will resolve Microsoft/vscode#45145
2018-03-09 07:11:53 -08:00
Daniel Imms ed7741e006 Merge pull request #1317 from bgw/rm-char-atlas-request
Merge ICharAtlasRequest with ICharAtlasConfig
2018-03-09 06:11:05 -08:00
Benjamin Woodruff cfe936a0b4 Merge ICharAtlasRequest with ICharAtlasConfig
ICharAtlasRequest and ICharAtlasConfig need almost exactly the same set
of information, so it's simpler if we just merge the two types.

As an added bonus, this also adds devicePixelRatio to the config, which
helps guarantee that we won't ever accidentally end up with an atlas
using a different pixel ratio than we need.
2018-03-08 23:23:33 -08:00
Daniel Imms 4ddf91dbbe Fix tests 2018-03-08 11:20:42 -08:00
Daniel Imms 59be054ca1 Fix exception for links on bottom line 2018-03-08 11:12:59 -08:00
Daniel Imms b794dc017d Fix test 2018-03-08 11:08:51 -08:00
Daniel Imms 4443ed69c7 Add noUnusedLocals tsconfig flag, fix issues
Two parts were commented out:
- _writeStopped: This is because _xoffSentToCatchUp references this variable
  and the feature is sort of half implemented/disabled.
- _clearChar: This is because some comments still reference this, it's part
  of the work needed to do better line redrawing.
2018-03-08 10:56:10 -08:00
Daniel Imms ab9b84898e Merge remote-tracking branch 'origin/master' into 24_multi_line_links 2018-03-08 10:23:05 -08:00
Daniel Imms a41e183b88 Merge pull request #1310 from Tyriar/426_alt_scroll
Send arrow events on mouse wheel in alt buffer
2018-03-08 09:53:36 -08:00
Daniel Imms 949239dbdc Merge remote-tracking branch 'origin/master' into 426_alt_scroll 2018-03-08 09:28:02 -08:00
Daniel Imms 421ccc5695 Join lines that wrap over more than 2 lines 2018-03-08 09:25:08 -08:00
Daniel Imms 5d68ed6e5e Merge pull request #1315 from Tyriar/underscore_private
Rename all remaining private variables to use underscores
2018-03-08 09:01:33 -08:00
Daniel Imms 021f8c1ba0 Draw and clear link cells 2018-03-08 07:00:37 -08:00
Daniel Imms f456b14b3f Get multi-line link hover working 2018-03-08 06:42:40 -08:00
Daniel Imms 7a1ef19146 Merge remote-tracking branch 'origin/master' into 24_multi_line_links 2018-03-08 06:23:29 -08:00
Daniel Imms b9cbdf6a0d Fix tests 2018-03-08 06:19:17 -08:00
Daniel Imms 7639131c12 Ensure underscore is used for private vars
Variables changed based on regex: "private [^(_|get)]"
2018-03-08 06:09:46 -08:00
Daniel Imms 289b4eeb23 Merge branch 'master' into 426_alt_scroll 2018-03-08 05:37:16 -08:00
Daniel Imms 404a285eaf Merge pull request #1307 from bgw/split-char-atlas-files
Split CharAtlas types and utility functions off
2018-03-08 05:16:44 -08:00
Daniel Imms b20fe80f9b Merge pull request #1308 from bgw/rm-did-char-size-change
Remove didCharSizeChange logic from renderer
2018-03-08 05:10:53 -08:00
Daniel Imms ff9814a40d Merge pull request #1293 from nikonso/allow-named-theme-colors-1195
Validate colors
2018-03-08 05:02:50 -08:00
Daniel Imms 855964fbe1 Merge pull request #1296 from olsonpm/fix-paste-newline
Paste newlines as \r
2018-03-08 05:02:31 -08:00
Paris Kasidiaris 606c68dd0c Bump version to 3.2.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-03-08 08:59:46 +00:00
Benjamin Woodruff 735dfb4710 Add copyright header to shared/atlas/Types.ts 2018-03-07 23:19:38 -08:00
Benjamin Woodruff ab69bfbb32 Move rest of atlas implementations to atlas dirs
Moves CharAtlas into src/renderer/atlas/ and CharAtlasGenerator into
src/shared/atlas/.
2018-03-07 22:23:19 -08:00
Benjamin Woodruff 70771430b5 Clean up char atlas constant imports/exports
- Ensures that everything is only defined in one place, and everything
  imports from that place.
- Moves CHAR_ATLAS_CELL_SPACING into a subdirectory of shared/ so that
  CharAtlasGenerator can pull from it.

This addresses the comments on
https://github.com/xtermjs/xterm.js/pull/1307/files/454fd4bfb1ab5c9ece
2018-03-07 22:13:41 -08:00
Paris Kasidiaris 6741e92cc6 Merge pull request #1313 from xtermjs/docker-image
Add xterm.js Docker image to documentation
2018-03-07 23:00:04 +02:00
Paris Kasidiaris 31ce5c1826 Run the webpack build in the docker image too 2018-03-07 12:38:29 +02:00
Paris Kasidiaris 1f113b1d29 Add xterm.js Docker image to documentation
Closes #1295
2018-03-07 12:28:44 +02:00
Bruno Ribeiro 41f6ee4f6d Merge branch 'master' into allow-named-theme-colors-1195 2018-03-06 20:15:32 +00:00
Daniel Imms 6e546c0f30 Merge pull request #1298 from Tyriar/1297_webLinks_addon
Pull web link support into an addon
2018-03-06 09:29:33 -08:00
Daniel Imms 7cae8e51ca Merge pull request #1290 from Tyriar/1287_double_click_exception
Check for valid coords before getting word at coords
2018-03-06 09:28:55 -08:00
Daniel Imms bf4dd7de60 Merge branch 'master' into 1287_double_click_exception 2018-03-06 09:28:47 -08:00
Thomas Zilz ff6ef1b63a Initialise _wheelPartialScroll to 0 to prevent NaN 2018-03-06 11:39:14 +01:00
Bruno Ribeiro 34034b849f Merge branch 'master' into allow-named-theme-colors-1195 2018-03-05 23:22:12 +00:00
Daniel Imms f2d8709dd8 Merge pull request #1309 from Tyriar/1305_alt_horizontal_only
Only move horizontally on alt click in normal buffer
2018-03-05 14:25:45 -08:00
Daniel Imms e137530581 Scroll by exact line amount in alt buffer, unify scroll logic
Fixes #1304
2018-03-05 14:22:20 -08:00
Daniel Imms be7788dda6 Merge remote-tracking branch 'origin/master' into 426_alt_scroll 2018-03-05 11:48:40 -08:00
Daniel Imms 7c471f5389 Fix test compile 2018-03-05 11:32:05 -08:00
Daniel Imms 9e62c8692f Only move horizontally on alt click in normal buffer
Fixes #1305
2018-03-05 11:11:54 -08:00
Benjamin Woodruff 18fb134910 Remove didCharSizeChange logic from renderer
The only thing `didCharSizeChange` was used for was to figure out if we
need to refresh the character atlas or not. However, `acquireCharAtlas`
already inspects the atlas owned by this terminal, and if it matches, it
avoids generating a new atlas.

So if `didCharSizeChange` is true and it's not needed, it quickly bails
out as a no-op. But if `didCharSizeChange` is false, and it was needed
(possible given the complexity of testing all of these edge cases), it
could introduce a bug.

I think it makes sense to just get rid of `didCharSizeChange`, and just
always call `acquireCharAtlas`.
2018-03-04 20:55:42 -08:00
Benjamin Woodruff 454fd4bfb1 Split CharAtlas types and utility functions off
I'm starting to pull some changes off of my large WIP branch/commit that
adds an alternative dynamic character atlas. We're going to need to
support multiple CharAtlas implementations, and this should make that
easier.
2018-03-04 18:50:46 -08:00
Daniel Imms 131e6ff949 Merge pull request #1302 from nikonso/Enforce-option-value-via-types
Enforce option value via types
2018-02-28 07:47:25 -08:00
Bruno Ribeito c51fa238ff Readd any overloads 2018-02-27 23:18:33 +00:00
Daniel Imms 2e3f9e7094 Multi-line links prototype
Part of #24
2018-02-27 08:37:16 -08:00
Bruno Ribeito 76a3dbf883 Enforce option value via types 2018-02-26 23:27:53 +00:00
Jon Austin 07cf029497 Merge branch 'master' into patch-1 2018-02-24 13:12:30 -08:00
Daniel Imms c8017345f1 Merge pull request #1288 from dgriffen/dev/dgriffen/fix-scrollbar
fix terminal width calculation
2018-02-24 11:56:35 -08:00
Daniel Imms 0d2cc7489e Fix tests, add webLinks addon test 2018-02-24 10:36:34 -08:00
Jon Austin f3a37a9522 Add Hyper to list of apps
https://github.com/zeit/hyper/blob/82c7afbeecf5a0c455075868cd23585fce95cf68/package.json#L197
2018-02-24 10:14:51 -08:00
Daniel Imms 6a71717751 Allow customization of the web links handler and options 2018-02-23 11:28:14 -08:00
Daniel Imms dc822787d0 Move http links to the new webLinks addon
Fixes #1297
2018-02-23 11:11:23 -08:00
Daniel Imms 5011e629b1 Build addons by scanning addon dir 2018-02-23 11:08:11 -08:00
Philip Olson bc35f28f51 Paste newlines as \r
Fix issue where non-windows OS terminals don't paste newlines correctly
into misbehaving programs.  As noted in JetBrains/jediterm#136, the
most practical solution is for terminals to handle this in their paste
logic.  In a perfect world, all terminal programs would implement
bracketed paste mode properly, but that's a lot to ask.

- translate newlines to \r which is what terminal programs expect
2018-02-22 23:52:45 -06:00
Bruno Ribeiro 97feb64e94 Merge branch 'master' into allow-named-theme-colors-1195 2018-02-21 23:51:36 +00:00
Daniel Imms 94bbc4e09d Merge pull request #1200 from nikonso/1181
WIP: Use web audio api
2018-02-21 15:36:19 -08:00
Daniel Imms ad7d2262ec Undo package-lock changes 2018-02-21 15:35:53 -08:00
Bruno Ribeito 1114977a21 Fix tests 2018-02-21 23:35:37 +00:00
Bruno Ribeiro a8c5b159a9 Merge branch 'master' into allow-named-theme-colors-1195 2018-02-21 23:17:00 +00:00
Daniel Imms a91e0e234a Use const everywhere 2018-02-21 14:44:48 -08:00
Daniel Imms af5b3f704e Fix lint 2018-02-21 14:42:04 -08:00
Daniel Imms 45c9763092 Fix indentation 2018-02-21 14:40:22 -08:00
Daniel Imms db8ba17147 Check audio context ctor before constructing 2018-02-21 14:39:43 -08:00
Daniel Imms 1972bf4bb3 Merge remote-tracking branch 'ups/master' into 1181 2018-02-21 14:36:11 -08:00
Daniel Imms 62cf0936dc Merge pull request #1231 from saamalik/ctrl-alt-sequences
Additional alt and control sequences
2018-02-21 14:08:31 -08:00
Bruno Ribeito a9d6686e14 Regex is not needed for color validation 2018-02-20 20:38:12 +00:00
Bruno Ribeito 99aa142cef Fix test -> wrong Regex flag 2018-02-19 23:55:25 +00:00
Bruno Ribeito f8f1355155 Validate colors 2018-02-19 22:54:09 +00:00
Daniel Imms ae28814041 Check for valid coords before getting word at coords
Fixes #1287
2018-02-16 21:34:57 -08:00
Daniel Griffen 183fd2fe79 fix terminal width calculation 2018-02-16 10:18:03 -08:00
Saad Malik 8324462b32 Merge branch 'master' into ctrl-alt-sequences 2018-02-15 09:49:16 -08:00
Daniel Imms 93e5996422 Merge pull request #1182 from Tyriar/731_screen_reader
Screen reader support
2018-02-15 06:20:14 -08:00
Daniel Imms f089b997c0 Merge branch 'master' into 731_screen_reader 2018-02-14 09:03:31 -08:00
Saad Malik 51d2064061 Merge branch 'master' into ctrl-alt-sequences 2018-02-13 14:59:46 -08:00
Daniel Imms 712b91a971 Merge pull request #1282 from matthew-matvei/add-freeman
Added FreeMAN to list of real-world uses
2018-02-13 06:37:55 -08:00
Daniel Imms cbe6c64a92 Merge pull request #1280 from AndrienkoAleksandr/fixHeadlessMode
Fix xterm.js in headless mode yields TypeError on resize
2018-02-12 12:42:14 -08:00
Matthew James 64ed8309a1 Added FreeMAN to list of real-world uses 2018-02-12 20:26:09 +00:00
Oleksandr Andriienko b5ba9ae771 Fix xterm.js in headless mode yields TypeError on resize
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-02-12 12:30:26 +02:00
Saad Malik 18f6556061 Merge branch 'master' into ctrl-alt-sequences 2018-02-09 14:00:24 -08:00
Saad Malik 6dc49b7d01 Merge branch 'master' into ctrl-alt-sequences 2018-02-08 12:03:38 -08:00
Daniel Imms 74a6dfb197 Send arrow events on mouse wheel in alt buffer
Fixes #426
2018-01-29 11:28:48 -08:00
Saad Malik 73e0db1912 Merge branch 'master' into ctrl-alt-sequences 2018-01-29 10:21:36 -08:00
Saad Malik 9ade1f0a5d Add mapping for backward slash
* Add mapping for backward slash
* Fix mapping for single/double quote
2018-01-28 17:11:13 -08:00
Saad Malik f8fbdf31fa Merge branch 'master' into ctrl-alt-sequences 2018-01-28 16:54:55 -08:00
Daniel Imms a0f390ef54 Expose strings through the API 2018-01-28 13:10:11 -08:00
Daniel Imms ec156f62e1 Only update a11y manager dimensions after a renderer resize 2018-01-27 18:38:28 -08:00
Daniel Imms a3958a1cba Merge branch 'master' into 731_screen_reader 2018-01-27 18:15:31 -08:00
Daniel Imms 7c8b64aaf1 Revert "Hook up scroll APIs to scroll and focus when navigating"
This reverts commit b13826441c.
2018-01-27 18:14:58 -08:00
Daniel Imms b13826441c Hook up scroll APIs to scroll and focus when navigating 2018-01-27 18:14:52 -08:00
Daniel Imms 0bdf1e8265 Remove unused function 2018-01-27 17:51:43 -08:00
Daniel Imms 5506af5322 Clean up a11y manager 2018-01-27 16:47:23 -08:00
Daniel Imms 6439aca1d8 Refresh dimensions of only a single row 2018-01-27 15:00:19 -08:00
Daniel Imms 65850c1c20 Reduce duplication 2018-01-27 14:53:46 -08:00
Daniel Imms 963db4d2fd Remove navigation mode/more rows element 2018-01-27 14:48:45 -08:00
Daniel Imms 5c061195c0 Get navigation right/down working with nav mode 2018-01-27 14:45:22 -08:00
Saad Malik 0104e4214e Reimplement using map
* Reimplemented reg + shift alt digits and characters using a map
* Add test cases for digits
2018-01-26 18:33:37 -08:00
Saad Malik baaf61b3a3 Merge branch 'master' into ctrl-alt-sequences 2018-01-26 13:38:48 -08:00
Daniel Imms b9976c3d17 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-26 10:24:09 -08:00
Daniel Imms 1cceda6052 Change navigation mode to work using focus instead of activedescendant 2018-01-24 16:34:39 -08:00
Daniel Imms 3e734fab79 Support i18n 2018-01-24 11:15:56 -08:00
Daniel Imms c41b35ae9c Add message about entering navigation mode 2018-01-24 11:04:41 -08:00
Saad Malik e478918ea7 Merge branch 'master' into ctrl-alt-sequences 2018-01-24 09:00:26 -08:00
Saad Malik e1c99b28f0 Merge branch 'master' into ctrl-alt-sequences 2018-01-23 11:16:47 -08:00
Daniel Imms 45c6008b27 Rotate terminal rows after removing focus 2018-01-23 11:11:29 -08:00
Daniel Imms fba4f5f261 Ensure active item is removed before scroll 2018-01-23 10:39:21 -08:00
Daniel Imms bce0cce74d Ensure a11y tree is located above the prompt 2018-01-23 09:50:47 -08:00
Daniel Imms c2d78e57d0 Fix screenReadeMode disable in demo 2018-01-23 09:49:43 -08:00
Saad Malik bb8e06592c Merge branch 'master' into ctrl-alt-sequences 2018-01-22 12:53:45 -08:00
Daniel Imms 421a885acd Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-22 06:32:38 -08:00
Saad Malik d62e4e25b7 Cleaup whitespace 2018-01-21 16:56:58 -08:00
Saad Malik ec9f2702eb Additional alt and control sequences
* Add punctuation alt sequences
* Add support for ctrl+alt sequences
2018-01-21 16:40:38 -08:00
Daniel Imms b97cedcf5b Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-19 14:01:37 -08:00
Daniel Imms 39fcbe61c0 Improve announcement on focus 2018-01-19 10:52:03 -08:00
Daniel Imms 0f04f9fbb4 Rotate rows to ensure an item will be read in nav mode when scrolling 2018-01-17 14:10:03 -08:00
Daniel Imms abfa58fca9 Keep track of nav mode focused element 2018-01-17 10:52:12 -08:00
Daniel Imms 78d734117b Support aria posinset and setsize 2018-01-17 09:56:57 -08:00
Daniel Imms f48da12c16 Prevent event propagation when handled by nav mode 2018-01-16 11:24:19 -08:00
Daniel Imms 3fa4b4941f Support nav mode pgup/pgdown/end/home, check boundaries 2018-01-16 11:01:45 -08:00
Daniel Imms 222c42f7ea Expose screen reader and navigation mode on demo/API 2018-01-16 10:29:40 -08:00
Daniel Imms 34eb08952c Fix RenderDebouncer not refreshing whole viewport when asked 2018-01-16 10:26:09 -08:00
Daniel Imms e9b55930d8 Fix test mocks 2018-01-16 10:12:37 -08:00
Daniel Imms 1e9fb8696a Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-16 09:56:19 -08:00
Daniel Imms 81b4d8fe9c Dispose of NavigationMode when AccessibilityManager is 2018-01-14 13:42:36 -08:00
Daniel Imms 23bd60a733 Properly dispose of disposables 2018-01-14 13:28:43 -08:00
Daniel Imms 85c02f10bf Improve dispoable listener functions 2018-01-14 13:18:00 -08:00
Daniel Imms 70489e5035 Move navigation mode stuff into its own class 2018-01-14 12:59:57 -08:00
Daniel Imms 1e41b11319 Remove logs 2018-01-12 21:15:53 -08:00
Daniel Imms 2a187a26e0 Update a11y rows on scroll 2018-01-11 23:15:02 -08:00
Daniel Imms 2311d7d66b Add basic navigation mode support for current viewport 2018-01-11 22:56:19 -08:00
Daniel Imms bbc9a010b9 Listen to dpr change in a11y manager 2018-01-11 09:27:37 -08:00
Bruno Ribeito b75f52734d Use Web audio API 2018-01-08 20:53:26 +00:00
Daniel Imms a19b05f813 Fix bad type check that caused rows to not refresh 2018-01-08 09:53:06 -08:00
Daniel Imms c46304c716 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-05 14:28:08 -08:00
Daniel Imms e20d2caa0e Use consistent refresh/render method names 2018-01-05 08:19:30 -08:00
Daniel Imms a846e8c08d Pull render animation frame logic into a helper class 2018-01-05 08:15:21 -08:00
Daniel Imms c332aeb51f Remove obsolete code and resolved TODOs 2018-01-05 07:51:22 -08:00
Daniel Imms bb830eb1ae Rate limit row refresh, tweak live region 2018-01-05 07:39:45 -08:00
Daniel Imms 6d1064fc01 Set height of a11y rows on creation 2018-01-03 10:08:24 -08:00
Daniel Imms 5ca15cfe3b Fix linefeed event
This was causing new lines to join words together
2018-01-03 09:54:06 -08:00
Daniel Imms 17964a517c Add screenReaderMode to typings 2018-01-03 09:28:24 -08:00
Daniel Imms 8cb7a9dabe Merge remote-tracking branch 'origin/v3' into 731_screen_reader 2018-01-03 09:09:42 -08:00
Daniel Imms 5dc57a1305 Add detach logic back for mac only 2017-12-20 11:06:32 -08:00
Daniel Imms 0d65cb3f69 Add max rows to read to handle spam case 2017-12-19 16:27:03 -08:00
Daniel Imms ae957ecb3e Get output reading working on Windows, refresh on window resize 2017-12-19 15:48:58 -08:00
Daniel Imms d03f5b0c2a Range of improvements for macOS VoiceOver 2017-12-19 15:16:28 -08:00
Daniel Imms e5d57b08c8 Merge remote-tracking branch 'ups/v3' into 731_screen_reader 2017-12-19 13:28:31 -08:00
Daniel Imms 399779692d Initial prototype 2017-12-19 12:57:58 -08:00
130 changed files with 8913 additions and 19251 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.
+3 -1
View File
@@ -15,9 +15,11 @@ build/
.vscode/
.DS_Store
fixtures/typings-test/*.js
package-lock.json
# Directories needed for code coverage
/coverage/
coverage/
.nyc_output/
# Keep bundled code out of Git
dist/
+44 -12
View File
@@ -1,16 +1,48 @@
node_modules/
*.swp
.lock-wscript
lib/*.test.js
lib/*.test.js.map
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Whitelist - entries to be included must be negated with "!"
!*.js
!*.json
# Whitelist - dist/
!dist/**/*.js
!dist/**/*.js.map
!dist/**/*.css
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
!src/**/*.d.ts
!src/**/*.js
!src/**/*.js.map
!src/**/*.css
# Whitelist - typings/
!typings/*.d.ts
# Blacklist - (normal behavior) these will override any whitelist
*.test.ts
*.test.d.ts
*.test.js
*.test.js.map
lib/test/
Makefile.gyp
*.Makefile
*.target.gyp.mk
*.node
example/*.log
docs/
npm-debug.log
/.idea/
.env
.vscode/
build/
fixtures/
coverage/
demo/
+1
View File
@@ -0,0 +1 @@
package-lock=false
+1 -2
View File
@@ -13,8 +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
after_success: npm run coveralls
-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
}
}
+25
View File
@@ -1,10 +1,13 @@
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>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
@@ -18,9 +21,12 @@ 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>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brandon Bayer <b@bayer.ws>
Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
@@ -32,6 +38,7 @@ Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
@@ -43,6 +50,7 @@ Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Exile <exile@chamber.ee>
Felipe Gasper <felipe@felipegasper.com>
Felix <30559812+felixse@users.noreply.github.com>
ficristo <ficristo.work@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
@@ -58,31 +66,45 @@ Jianhui Zhao <jianhuizhao329@gmail.com>
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>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Ledion Bitincka <lbitincka@gmail.com>
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>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
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>
Paris Kasidiaris <pariskasidiaris@gmail.com>
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>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
@@ -93,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>
@@ -100,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
+4
View File
@@ -50,3 +50,7 @@ By contributing code to xterm.js you
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Third party dependencies
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
+1 -1
View File
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run test
RUN npm run build && npm run webpack && npm run test
+60 -11
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/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![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,9 +39,9 @@ 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 \033[1;3;31mxterm.js\033[0m $ ')
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
@@ -57,8 +57,6 @@ The proposed way to load xterm.js is via the ES6 module syntax.
import { Terminal } from 'xterm';
```
*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.*
### Addons
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
@@ -76,6 +74,27 @@ var xterm = new Terminal(); // Instantiate the terminal
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
```
#### Importing Addons in TypeScript
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
```typescript
import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit';
const xterm = new Terminal();
// Fit the terminal when necessary:
fit(xterm);
```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
@@ -110,7 +129,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
@@ -126,8 +145,16 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**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
@@ -143,7 +170,19 @@ Then open your project's [Public URL](https://help.sourcelair.com/projects/the-p
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
To run the just demo (with no editing access). run the following command in your terminal:
```
docker run -p 3000:3000 xtermjs/xterm.js
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
@@ -188,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.
@@ -202,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 -74
View File
@@ -1,77 +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>xterm.js: xterm, in the browser</h1>
<div id="terminal-container"></div>
<div>
<h2>Actions</h2>
<p>
<label>Find next <input id="find-next"/></label>
<label>Find previous <input id="find-previous"/></label>
</p>
</div>
<div>
<h2>Options</h2>
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</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>
<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>
<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>
+150 -68
View File
@@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
@@ -24,82 +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'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
},
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.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
optionElements.bellStyle.addEventListener('change', function () {
term.setOption('bellStyle', optionElements.bellStyle.value);
});
optionElements.macOptionIsMeta.addEventListener('change', function () {
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
});
optionElements.scrollback.addEventListener('change', function () {
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
});
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
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)
});
term = new Terminal({});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) {
if (!pid) {
@@ -116,17 +74,34 @@ function createTerminal() {
term.open(terminalContainer);
term.winptyCompatInit();
term.webLinksInit();
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) {
@@ -166,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
);
@@ -181,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;
}
@@ -7,16 +7,16 @@ var pty = require('node-pty');
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../../../../build'));
app.use('/demo', express.static(__dirname + '/../../../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../../../node_modules/zmodem.js/dist'));
app.use('/build', express.static(__dirname + '/../../build'));
app.use('/demo', express.static(__dirname + '/../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
res.sendFile(__dirname + '../style.css');
});
app.get('/main.js', function(req, res){
@@ -6,6 +6,11 @@ var term,
socket,
pid;
Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
Terminal.applyAddon(zmodem);
Terminal.applyAddon(search);
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
+1
View File
@@ -2,6 +2,7 @@ version: "3"
services:
web:
image: xtermjs/xterm.js:latest
build: .
volumes:
- ./:/usr/src/app
+25 -81
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,10 +18,17 @@ 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 = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
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.
@@ -31,58 +36,19 @@ 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']);
-26
View File
@@ -1,26 +0,0 @@
{
"source": {
"include": [
"src/"
],
"exclude": [
"src/test/"
],
"excludePattern": "src/.+\\.test\\.(js|ts)$"
},
"opts": {
"readme": "README.md",
"template": "node_modules/docdash",
"encoding": "utf8",
"destination": "docs/",
"recurse": true,
"verbose": true
},
"plugins": [
"plugins/markdown"
],
"templates": {
"cleverLinks": false,
"monospaceLinks": false
}
}
-15661
View File
File diff suppressed because it is too large Load Diff
+25 -52
View File
@@ -1,52 +1,21 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.1.0",
"ignore": [
"demo",
"test",
".gitignore"
],
"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",
"files": [
"*.js",
"*.json",
"dist/*.css",
"dist/**/*.css",
"dist/*.js",
"dist/*.js.map",
"dist/**/*.js",
"dist/**/*.js.map",
"lib/*.css",
"lib/**/*.css",
"lib/*.d.ts",
"lib/*.js",
"lib/*.js.map",
"lib/**/*.d.ts",
"lib/**/*.js",
"lib/**/*.js.map",
"src/*.css",
"src/**/*.css",
"src/*.js",
"src/*.js.map",
"src/*.ts",
"src/**/*.js",
"src/**/*.js.map",
"src/**/*.ts",
"typings/*.d.ts"
],
"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.41",
"@types/text-encoding": "0.0.32",
"@types/node": "6.0.108",
"browserify": "^13.3.0",
"chai": "3.5.0",
"docdash": "0.4.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",
@@ -54,20 +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",
"typescript": "~2.7.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "2.8.3",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
@@ -75,18 +45,21 @@
"zmodem.js": "^0.1.5"
},
"scripts": {
"start": "node demo/app",
"prestart-zmodem": "npm run build",
"start-zmodem": "node build/addons/zmodem/demo/app",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test",
"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"
},
"dependencies": {}
"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\""
}
}
+280
View File
@@ -0,0 +1,280 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './ui/RenderDebouncer';
import { addDisposableDomListener } from './ui/Lifecycle';
import { Disposable } from './common/Lifecycle';
const MAX_ROWS_TO_READ = 20;
const enum BoundaryPosition {
TOP,
BOTTOM
}
export class AccessibilityManager extends Disposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
/**
* 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
* not announced (added to live region) because it has already been announced
* by the textarea event (which cannot be canceled). There are some race
* condition cases if there is typing while data is streaming, but this covers
* the main case of typing into the prompt and inputting the answer to a
* question (Y/N, etc.).
*/
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
super();
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
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.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.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.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
super.dispose();
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.TOP) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop()!;
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift()!;
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.TOP) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
private _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _onTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
}
}
private _onChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._announceCharacter(char);
}
} else {
this._announceCharacter(char);
}
if (char === '\n') {
this._liveRegionLineCount++;
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
this._liveRegion.textContent += Strings.tooMuchOutput;
}
}
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
}, 0);
}
}
}
}
private _clearLiveRegion(): void {
this._liveRegion.textContent = '';
this._liveRegionLineCount = 0;
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.parentNode) {
this._accessibilityTreeRoot.removeChild(this._liveRegion);
}
}
}
private _onKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
private _refreshRowsDimensions(): void {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
return;
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
}
private _announceCharacter(char: string): void {
if (char === ' ') {
// Always use nbsp for spaces in order to preserve the space between characters in
// voiceover's caption window
this._liveRegion.innerHTML += '&nbsp;';
} else {
this._liveRegion.textContent += char;
}
}
}
+159 -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', () => {
@@ -188,4 +245,105 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS / 2);
});
});
describe('addMarker', () => {
it('should adjust a marker line when the buffer is trimmed', () => {
terminal.options.scrollback = 0;
buffer = new Buffer(terminal, true);
buffer.fillViewportRows();
const marker = buffer.addMarker(buffer.lines.length - 1);
assert.equal(marker.line, buffer.lines.length - 1);
buffer.lines.emit('trim', 1);
assert.equal(marker.line, buffer.lines.length - 2);
});
it('should dispose of a marker if it is trimmed off the buffer', () => {
terminal.options.scrollback = 0;
buffer = new Buffer(terminal, true);
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
assert.equal(marker.isDisposed, false);
assert.equal(buffer.markers.length, 1);
buffer.lines.emit('trim', 1);
assert.equal(marker.isDisposed, true);
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');
});
});
});
+84 -27
View File
@@ -3,9 +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 { 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;
@@ -20,8 +23,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
* - scroll position
*/
export class Buffer implements IBuffer {
private _lines: CircularList<LineData>;
public lines: CircularList<LineData>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -31,6 +33,7 @@ export class Buffer implements IBuffer {
public tabs: any;
public savedY: number;
public savedX: number;
public markers: Marker[] = [];
/**
* Create a new Buffer.
@@ -45,10 +48,6 @@ export class Buffer implements IBuffer {
this.clear();
}
public get lines(): CircularList<LineData> {
return this._lines;
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
@@ -78,7 +77,7 @@ export class Buffer implements IBuffer {
* Fills the buffer's viewport with blank lines.
*/
public fillViewportRows(): void {
if (this._lines.length === 0) {
if (this.lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(this._terminal.blankLine());
@@ -94,7 +93,7 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
@@ -109,19 +108,19 @@ export class Buffer implements IBuffer {
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
if (newMaxLength > this._lines.maxLength) {
this._lines.maxLength = newMaxLength;
if (newMaxLength > this.lines.maxLength) {
this.lines.maxLength = newMaxLength;
}
// The following adjustments should only happen if the buffer has been
// initialized/filled.
if (this._lines.length > 0) {
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?
for (let i = 0; i < this._lines.length; i++) {
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
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);
}
}
}
@@ -130,8 +129,8 @@ export class Buffer implements IBuffer {
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
if (this.lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
@@ -143,16 +142,16 @@ export class Buffer implements IBuffer {
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
if (this.lines.length > newRows + this.ybase) {
if (this.lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
this.lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
@@ -164,15 +163,15 @@ export class Buffer implements IBuffer {
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
if (newMaxLength < this.lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
const amountToTrim = this.lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
}
this._lines.maxLength = newMaxLength;
this.lines.maxLength = newMaxLength;
}
// Make sure that the cursor stays on screen
@@ -228,7 +227,7 @@ export class Buffer implements IBuffer {
if (startCol >= i) {
startIndex--;
}
if (endCol >= i) {
if (endCol > i) {
endIndex--;
}
} else {
@@ -261,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.
@@ -303,4 +316,48 @@ export class Buffer implements IBuffer {
while (!this.tabs[++x] && x < this._terminal.cols);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
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.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
return marker;
}
private _removeMarker(marker: Marker): void {
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
this.markers.splice(this.markers.indexOf(marker), 1);
}
}
export class Marker extends EventEmitter implements IMarker {
private static _nextId = 1;
private _id: number = Marker._nextId++;
public isDisposed: boolean = false;
public get id(): number { return this._id; }
constructor(
public line: number
) {
super();
}
public dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
// Emit before super.dispose such that dispose listeners get a change to react
this.emit('dispose');
super.dispose();
}
}
+3 -3
View File
@@ -49,7 +49,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
@@ -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;
}
+34 -33
View File
@@ -5,19 +5,20 @@
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 = {
classList: {
add: () => {},
remove: () => {},
remove: () => {}
},
getBoundingClientRect: () => {
return { width: 0 };
@@ -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();
+63 -64
View File
@@ -20,43 +20,43 @@ export class CompositionHelper {
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
* IME. This variable determines whether the compositionText should be displayed on the UI.
*/
private isComposing: boolean;
private _isComposing: boolean;
/**
* The position within the input textarea's value of the current composition.
*/
private compositionPosition: IPosition;
private _compositionPosition: IPosition;
/**
* Whether a composition is in the process of being sent, setting this to false will cancel any
* in-progress composition.
*/
private isSendingComposition: boolean;
private _isSendingComposition: boolean;
/**
* Creates a new CompositionHelper.
* @param textarea The textarea that xterm uses for input.
* @param compositionView The element to display the in-progress composition in.
* @param terminal The Terminal to forward the finished composition to.
* @param _textarea The textarea that xterm uses for input.
* @param _compositionView The element to display the in-progress composition in.
* @param _terminal The Terminal to forward the finished composition to.
*/
constructor(
private textarea: HTMLTextAreaElement,
private compositionView: HTMLElement,
private terminal: ITerminal
private _textarea: HTMLTextAreaElement,
private _compositionView: HTMLElement,
private _terminal: ITerminal
) {
this.isComposing = false;
this.isSendingComposition = false;
this.compositionPosition = { start: null, end: null };
this._isComposing = false;
this._isSendingComposition = false;
this._compositionPosition = { start: null, end: null };
}
/**
* Handles the compositionstart event, activating the composition view.
*/
public compositionstart(): void {
this.isComposing = true;
this.compositionPosition.start = this.textarea.value.length;
this.compositionView.textContent = '';
this.compositionView.classList.add('active');
this._isComposing = true;
this._compositionPosition.start = this._textarea.value.length;
this._compositionView.textContent = '';
this._compositionView.classList.add('active');
}
/**
@@ -64,10 +64,10 @@ export class CompositionHelper {
* @param {CompositionEvent} ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
this.compositionView.textContent = ev.data;
this._compositionView.textContent = ev.data;
this.updateCompositionElements();
setTimeout(() => {
this.compositionPosition.end = this.textarea.value.length;
this._compositionPosition.end = this._textarea.value.length;
}, 0);
}
@@ -76,7 +76,7 @@ export class CompositionHelper {
* the handler.
*/
public compositionend(): void {
this.finalizeComposition(true);
this._finalizeComposition(true);
}
/**
@@ -85,24 +85,23 @@ export class CompositionHelper {
* @return Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
if (this.isComposing || this.isSendingComposition) {
if (this._isComposing || this._isSendingComposition) {
if (ev.keyCode === 229) {
// Continue composing if the keyCode is the "composition character"
return false;
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
// Continue composing if the keyCode is a modifier key
return false;
} else {
// Finish composition immediately. This is mainly here for the case where enter is
// pressed and the handler needs to be triggered before the command is executed.
this.finalizeComposition(false);
}
// Finish composition immediately. This is mainly here for the case where enter is
// pressed and the handler needs to be triggered before the command is executed.
this._finalizeComposition(false);
}
if (ev.keyCode === 229) {
// If the "composition character" is used but gets to this point it means a non-composition
// character (eg. numbers and punctuation) was pressed when the IME was active.
this.handleAnyTextareaChanges();
this._handleAnyTextareaChanges();
return false;
}
@@ -117,22 +116,22 @@ export class CompositionHelper {
* compositionend event is triggered, such as enter, so that the composition is send before
* the command is executed.
*/
private finalizeComposition(waitForPropogation: boolean): void {
this.compositionView.classList.remove('active');
this.isComposing = false;
this.clearTextareaPosition();
private _finalizeComposition(waitForPropogation: boolean): void {
this._compositionView.classList.remove('active');
this._isComposing = false;
this._clearTextareaPosition();
if (!waitForPropogation) {
// Cancel any delayed composition send requests and send the input immediately.
this.isSendingComposition = false;
const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
this.terminal.handler(input);
this._isSendingComposition = false;
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
this._terminal.handler(input);
} else {
// Make a deep copy of the composition position here as a new compositionstart event may
// fire before the setTimeout executes.
const currentCompositionPosition = {
start: this.compositionPosition.start,
end: this.compositionPosition.end,
start: this._compositionPosition.start,
end: this._compositionPosition.end
};
// Since composition* events happen before the changes take place in the textarea on most
@@ -143,22 +142,22 @@ export class CompositionHelper {
// - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to
// the following character if the following input is a vowel.
this.isSendingComposition = true;
this._isSendingComposition = true;
setTimeout(() => {
// Ensure that the input has not already been sent
if (this.isSendingComposition) {
this.isSendingComposition = false;
if (this._isSendingComposition) {
this._isSendingComposition = false;
let input;
if (this.isComposing) {
if (this._isComposing) {
// Use the end position to get the string if a new composition has started.
input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
// (eg. 2) after a composition character.
input = this.textarea.value.substring(currentCompositionPosition.start);
input = this._textarea.value.substring(currentCompositionPosition.start);
}
this.terminal.handler(input);
this._terminal.handler(input);
}
}, 0);
}
@@ -170,15 +169,15 @@ export class CompositionHelper {
* character" (229) is triggered, in order to allow non-composition text to be entered when an
* IME is active.
*/
private handleAnyTextareaChanges(): void {
const oldValue = this.textarea.value;
private _handleAnyTextareaChanges(): void {
const oldValue = this._textarea.value;
setTimeout(() => {
// Ignore if a composition has started since the timeout
if (!this.isComposing) {
const newValue = this.textarea.value;
if (!this._isComposing) {
const newValue = this._textarea.value;
const diff = newValue.replace(oldValue, '');
if (diff.length > 0) {
this.terminal.handler(diff);
this._terminal.handler(diff);
}
}
}, 0);
@@ -191,27 +190,27 @@ export class CompositionHelper {
* necessary as the IME events across browsers are not consistently triggered.
*/
public updateCompositionElements(dontRecurse?: boolean): void {
if (!this.isComposing) {
if (!this._isComposing) {
return;
}
if (this.terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
const cursorTop = this.terminal.buffer.y * cellHeight;
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
if (this._terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight);
const cursorTop = this._terminal.buffer.y * cellHeight;
const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width;
this.compositionView.style.left = cursorLeft + 'px';
this.compositionView.style.top = cursorTop + 'px';
this.compositionView.style.height = cellHeight + 'px';
this.compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this.compositionView.getBoundingClientRect();
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = compositionViewBounds.width + 'px';
this.textarea.style.height = compositionViewBounds.height + 'px';
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
if (!dontRecurse) {
@@ -223,8 +222,8 @@ export class CompositionHelper {
* Clears the textarea's position so that the cursor does not blink on IE.
* @private
*/
private clearTextareaPosition(): void {
this.textarea.style.left = '';
this.textarea.style.top = '';
private _clearTextareaPosition(): void {
this._textarea.style.left = '';
this._textarea.style.top = '';
}
}
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;
}
}
+33 -9
View File
@@ -3,28 +3,51 @@
* @license MIT
*/
import { IEventEmitter } from 'xterm';
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: ((...args: any[]) => void)[]};
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 || {};
}
public on(type: string, listener: ((...args: any[]) => void)): void {
public on(type: string, listener: XtermListener): void {
this._events[type] = this._events[type] || [];
this._events[type].push(listener);
}
public off(type: string, listener: ((...args: any[]) => void)): void {
/**
* Adds a disposabe listener to the EventEmitter, returning the disposable.
* @param type The event type.
* @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: () => {
if (!handler) {
// Already disposed
return;
}
this.off(type, handler);
handler = null;
}
};
}
public off(type: string, listener: XtermListener): void {
if (!this._events[type]) {
return;
}
let obj = this._events[type];
const obj = this._events[type];
let i = obj.length;
while (i--) {
@@ -45,17 +68,18 @@ export class EventEmitter implements IEventEmitter {
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);
}
}
public listeners(type: string): ((...args: any[]) => void)[] {
public listeners(type: string): XtermListener[] {
return this._events[type] || [];
}
protected destroy(): void {
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);
});
});
+688 -155
View File
File diff suppressed because it is too large Load Diff
+57 -36
View File
@@ -4,16 +4,16 @@
*/
import { assert } from 'chai';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types';
import { IMouseZoneManager, IMouseZone } from './ui/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { MockBuffer } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './common/CircularList';
class TestLinkifier extends Linkifier {
constructor(_terminal: IBufferAccessor & IElementAccessor) {
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 {
@@ -32,23 +34,22 @@ class TestMouseZoneManager implements IMouseZoneManager {
}
describe('Linkifier', () => {
let terminal: IBufferAccessor & IElementAccessor;
let terminal: ITerminal;
let linkifier: TestLinkifier;
let mouseZoneManager: TestMouseZoneManager;
beforeEach(() => {
terminal = {
buffer: new MockBuffer(),
element: <HTMLElement>{}
};
terminal.buffer.lines = new CircularList<LineData>(20);
terminal = new MockTerminal();
terminal.cols = 100;
terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
});
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)]);
}
@@ -59,17 +60,6 @@ describe('Linkifier', () => {
terminal.buffer.lines.push(stringToRow(text));
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRows();
setTimeout(() => {
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
done();
}, 0);
}
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
@@ -80,7 +70,25 @@ describe('Linkifier', () => {
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length);
assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length);
});
done();
}, 0);
}
function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
@@ -101,12 +109,6 @@ describe('Linkifier', () => {
linkifier.attachToDom(mouseZoneManager);
});
describe('http links', () => {
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => {
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
@@ -135,6 +137,24 @@ describe('Linkifier', () => {
// character (U+1F537) which caused the path to be duplicated. See #642.
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
});
describe('multi-line links', () => {
it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done);
});
it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done);
});
it('should match links that start and end on line 2/2 of a wrapped line', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done);
});
it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
});
});
});
describe('validationCallback', () => {
@@ -147,7 +167,8 @@ describe('Linkifier', () => {
assert.equal(mouseZoneManager.zones.length, 1);
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, 5);
assert.equal(mouseZoneManager.zones[0].y, 1);
assert.equal(mouseZoneManager.zones[0].y1, 1);
assert.equal(mouseZoneManager.zones[0].y2, 1);
// Fires done()
mouseZoneManager.zones[0].clickCallback(<any>{});
}
@@ -200,19 +221,19 @@ describe('Linkifier', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
});
});
+55 -67
View File
@@ -3,35 +3,11 @@
* @license MIT
*/
import { IMouseZoneManager } from './input/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { IMouseZoneManager } from './ui/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './ui/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
/**
* The ID of the built in http(s) link matcher.
*/
const HYPERTEXT_LINK_MATCHER_ID = 0;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
@@ -41,24 +17,23 @@ 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[] = [];
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
constructor(
protected _terminal: IBufferAccessor & IElementAccessor
protected _terminal: ITerminal
) {
super();
this._rowsToLinkify = {
start: null,
end: null
};
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
@@ -111,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._rowsToLinkify.end = null;
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior for
* tandard http(s) links.
* @param handler The handler to use, this can be cleared with null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
}
/**
* Attaches a validation callback for hypertext links.
* @param callback The callback to use, this can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -139,7 +97,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
if (!handler) {
throw new Error('handler must be defined');
}
const matcher: ILinkMatcher = {
@@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
@@ -200,11 +157,32 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param rowIndex The index of the row to linkify.
*/
private _linkifyRow(rowIndex: number): void {
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
// Ensure the row exists
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
return;
}
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
// Only attempt to linkify rows that start in the viewport
if (rowIndex !== 0) {
return;
}
// If the first row is wrapped, backtrack to find the origin row and linkify that
do {
rowIndex--;
absoluteRowIndex--;
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
}
// Construct full unwrapped line text
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
let currentIndex = absoluteRowIndex + 1;
while (currentIndex < this._terminal.buffer.lines.length &&
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
}
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
}
@@ -220,16 +198,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
// 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);
@@ -265,10 +239,20 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param matcher The link matcher for the link.
*/
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + uri.length) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
if (x2 === 0) {
x2 = this._terminal.cols;
y2--;
}
this._mouseZoneManager.add(new MouseZone(
x + 1,
x + 1 + uri.length,
y + 1,
x1 + 1,
y1 + 1,
x2 + 1,
y2 + 1,
e => {
if (matcher.handler) {
return matcher.handler(e, uri);
@@ -276,18 +260,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer';
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.add('xterm-cursor-pointer');
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <ILinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, <ILinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = '';
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
@@ -300,4 +284,8 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols };
}
}
-674
View File
@@ -1,674 +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 } 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 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 j;
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':
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--;
// }
}
+112 -14
View File
@@ -3,14 +3,12 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
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, ICircularList, IBuffer } from './Types';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
class TestMockTerminal extends MockTerminal {
@@ -27,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); }
@@ -37,19 +37,11 @@ class TestSelectionManager extends SelectionManager {
}
describe('SelectionManager', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let terminal: ITerminal;
let buffer: IBuffer;
let rowContainer: HTMLElement;
let selectionManager: TestSelectionManager;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
terminal = new TestMockTerminal();
terminal.cols = 80;
terminal.rows = 2;
@@ -61,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)]);
}
@@ -308,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', () => {
@@ -324,6 +326,45 @@ describe('SelectionManager', () => {
});
});
describe('selectLines', () => {
it('should select a single line', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
buffer.lines.set(2, stringToRow('3'));
selectionManager.selectLines(1, 1);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
});
it('should select multiple lines', () => {
buffer.lines.length = 5;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
buffer.lines.set(2, stringToRow('3'));
buffer.lines.set(3, stringToRow('4'));
buffer.lines.set(4, stringToRow('5'));
selectionManager.selectLines(1, 3);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]);
});
it('should select the to the start when requesting a negative row', () => {
buffer.lines.length = 2;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
selectionManager.selectLines(-1, 0);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]);
});
it('should select the to the end when requesting beyond the final row', () => {
buffer.lines.length = 2;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
selectionManager.selectLines(1, 2);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
});
});
describe('hasSelection', () => {
it('should return whether there is a selection', () => {
selectionManager.model.selectionStart = [0, 0];
@@ -339,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');
});
});
});
+84 -33
View File
@@ -3,11 +3,10 @@
* @license MIT
*/
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData } from './Types';
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 { CircularList } from './utils/CircularList';
import { CharMeasure } from './ui/CharMeasure';
import { EventEmitter } from './EventEmitter';
import { SelectionModel } from './SelectionModel';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
@@ -55,10 +54,11 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
enum SelectionMode {
export const enum SelectionMode {
NORMAL,
WORD,
LINE
LINE,
COLUMN
}
/**
@@ -81,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
@@ -101,7 +101,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: (...args: any[]) => void;
private _trimListener: XtermListener;
private _mouseDownTimeStamp: number;
@@ -117,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;
}
@@ -178,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);
}
}
}
@@ -250,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
});
}
/**
@@ -293,6 +315,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.emit('selection');
}
public selectLines(start: number, end: number): void {
this._model.clearSelection();
start = Math.max(start, 0);
end = Math.min(end, this._terminal.buffer.lines.length - 1);
this._model.selectionStart = [0, start];
this._model.selectionEnd = [this._terminal.cols, end];
this.refresh();
this._terminal.emit('selection');
}
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
@@ -349,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;
}
/**
@@ -440,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);
@@ -493,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.
@@ -577,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();
@@ -634,6 +678,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
}
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
@@ -789,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;
}
}
-3
View File
@@ -18,9 +18,6 @@ class TestSelectionModel extends SelectionModel {
}
describe('SelectionManager', () => {
let window: Window;
let document: Document;
let terminal: ITerminal;
let model: TestSelectionModel;
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ISoundManager } from './Types';
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager {
private _audioContext: AudioContext;
constructor(
private _terminal: ITerminal
) {
}
public playBellSound(): void {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!this._audioContext && audioContextCtor) {
this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
}
}
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
private _removeMimeType(dataURI: string): string {
// Split the input to get the mime-type and the data itself
const splitUri = dataURI.split(',');
// Return only the data
return splitUri[1];
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export let blankLine = 'Blank line';
export let promptLabel = 'Terminal input';
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
+24 -18
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,7 +123,7 @@ 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(){};
@@ -127,12 +132,13 @@ if (os.platform() !== 'win32') {
xterm.writeBuffer.push(fromPty);
xterm.innerWrite();
let fromEmulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
const fromEmulator = terminalToString(xterm);
console.log = consoleLog;
const 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'));
+94 -207
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,27 +12,27 @@ 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); }
}
describe('term.js addons', () => {
let term: TestTerminal;
const termOptions = {
cols: INIT_COLS,
rows: INIT_ROWS
};
beforeEach(() => {
term = new TestTerminal({
cols: INIT_COLS,
rows: INIT_ROWS
});
term = new TestTerminal(termOptions);
term.refresh = () => {};
(<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(<any>term).compositionHelper = new MockCompositionHelper();
(<any>term)._compositionHelper = new MockCompositionHelper();
// Force synchronous writes
term.write = (data) => {
term.writeBuffer.push(data);
(<any>term).innerWrite();
(<any>term)._innerWrite();
};
(<any>term).element = {
classList: {
@@ -43,11 +42,13 @@ 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');
it('should not mutate the options parameter', () => {
term.setOption('cols', 1000);
assert.deepEqual(termOptions, {
cols: INIT_COLS,
rows: INIT_ROWS
});
});
describe('getOption', () => {
@@ -67,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',
@@ -119,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);
@@ -138,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);
@@ -150,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);
@@ -166,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');
@@ -201,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');
@@ -238,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');
@@ -258,22 +270,47 @@ describe('term.js addons', () => {
});
});
describe('keyDown', () => {
describe('scrollToLine', () => {
let startYDisp: number;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
}
startYDisp = (term.rows * 2) + 1;
});
it('should scroll to requested line', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(0);
assert.equal(term.buffer.ydisp, 0);
term.scrollToLine(10);
assert.equal(term.buffer.ydisp, 10);
term.scrollToLine(startYDisp);
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(20);
assert.equal(term.buffer.ydisp, 20);
});
it('should not scroll beyond boundary lines', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(-1);
assert.equal(term.buffer.ydisp, 0);
term.scrollToLine(startYDisp + 1);
assert.equal(term.buffer.ydisp, startYDisp);
});
});
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);
@@ -284,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;
});
@@ -292,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);
});
});
@@ -430,159 +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~');
});
});
describe('Third level shift', () => {
let evKeyDown;
let evKeyPress;
let evKeyDown: any;
let evKeyPress: any;
beforeEach(() => {
term.handler = () => {};
@@ -653,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);
}
@@ -719,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);
}
@@ -764,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);
@@ -776,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));
@@ -787,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;
@@ -800,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;
@@ -813,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);
@@ -848,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);
@@ -862,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);
@@ -890,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);
@@ -901,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);
@@ -911,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);
@@ -936,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);
@@ -960,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);
@@ -971,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);
@@ -981,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);
@@ -1005,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);
@@ -1016,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);
+459 -767
View File
File diff suppressed because it is too large Load Diff
+192 -33
View File
@@ -3,19 +3,22 @@
* @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;
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
@@ -41,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;
@@ -82,11 +83,13 @@ export interface IInputHandlingTerminal extends IEventEmitter {
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
tabSet(): void;
}
export interface IViewport {
export interface IViewport extends IDisposable {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
@@ -102,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;
@@ -131,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 {
@@ -165,14 +192,12 @@ export interface ILinkMatcher {
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x: number;
y: number;
length: number;
x1: number;
y1: number;
x2: number;
y2: number;
cols: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
@@ -184,7 +209,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
buffers: IBufferSet;
@@ -220,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 };
}
@@ -232,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;
@@ -242,7 +267,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
}
export interface IBuffer {
lines: ICircularList<LineData>;
readonly lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
@@ -250,10 +275,12 @@ export interface IBuffer {
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
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;
}
@@ -270,7 +297,6 @@ export interface IBufferSet extends IEventEmitter {
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
@@ -296,8 +322,6 @@ export interface ISelectionManager {
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
@@ -347,3 +371,138 @@ export interface IBrowser {
isIphone: boolean;
isMSWindows: boolean;
}
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;
}
+97 -55
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,53 +15,60 @@ 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;
private lastRecordedViewportHeight: number = 0;
private lastRecordedBufferHeight: number = 0;
private lastTouchY: number;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
private _wheelPartialScroll: number = 0;
/**
* Creates a new Viewport.
* @param terminal The terminal this viewport belongs to.
* @param viewportElement The DOM element acting as the viewport.
* @param scrollArea The DOM element acting as the scroll area.
* @param charMeasure A DOM element used to measure the character size of. the terminal.
* @param _terminal The terminal this viewport belongs to.
* @param _viewportElement The DOM element acting as the viewport.
* @param _scrollArea The DOM element acting as the scroll area.
* @param _charMeasure A DOM element used to measure the character size of. the terminal.
*/
constructor(
private terminal: ITerminal,
private viewportElement: HTMLElement,
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
private _terminal: ITerminal,
private _viewportElement: HTMLElement,
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.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChanged(colors: IColorSet): void {
this.viewportElement.style.backgroundColor = colors.background;
this._viewportElement.style.backgroundColor = colors.background.css;
}
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
private _refresh(): void {
if (this._charMeasure.height > 0) {
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
this._lastRecordedBufferHeight = newBufferHeight;
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
}
}
}
@@ -68,24 +77,24 @@ export class Viewport implements IViewport {
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
this._refresh();
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
this.refresh();
this._refresh();
} else {
// If size has changed, refresh viewport
if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) {
this.refresh();
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
}
}
// Sync scrollTop
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
if (this.viewportElement.scrollTop !== scrollTop) {
this.viewportElement.scrollTop = scrollTop;
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
this._viewportElement.scrollTop = scrollTop;
}
}
@@ -94,16 +103,16 @@ export class Viewport implements IViewport {
* terminal to scroll to it.
* @param ev The scroll event.
*/
private onScroll(ev: Event): void {
private _onScroll(ev: Event): void {
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this.viewportElement.offsetParent) {
if (!this._viewportElement.offsetParent) {
return;
}
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollLines(diff, true);
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true);
}
/**
@@ -113,28 +122,61 @@ export class Viewport implements IViewport {
* @param ev The mouse wheel event.
*/
public onWheel(ev: WheelEvent): void {
if (ev.deltaY === 0) {
// Do nothing if it's not a vertical scroll event
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return;
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let multiplier = 1;
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
multiplier = this.currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
multiplier = this.currentRowHeight * this.terminal.rows;
}
this.viewportElement.scrollTop += ev.deltaY * multiplier;
this._viewportElement.scrollTop += amount;
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
}
private _getPixelsScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let amount = ev.deltaY;
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._currentRowHeight * this._terminal.rows;
}
return amount;
}
/**
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
* is being used.
* @param ev The mouse wheel event.
*/
public getLinesScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_LINE
let amount = ev.deltaY;
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._terminal.rows;
}
return amount;
}
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
public onTouchStart(ev: TouchEvent): void {
this.lastTouchY = ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
}
/**
@@ -142,12 +184,12 @@ export class Viewport implements IViewport {
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): void {
let deltaY = this.lastTouchY - ev.touches[0].pageY;
this.lastTouchY = ev.touches[0].pageY;
const deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return;
}
this.viewportElement.scrollTop += deltaY;
this._viewportElement.scrollTop += deltaY;
ev.preventDefault();
}
}
+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;
+54 -21
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,29 +36,50 @@ 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 (ev.data instanceof ArrayBuffer) {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
str = myTextDecoder.decode( ev.data );
} else {
throw 'TODO: handle Blob?';
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
}
if (ev.data instanceof ArrayBuffer) {
str = myTextDecoder.decode(ev.data);
displayData(str);
} else {
const fileReader = new FileReader();
if (buffered) {
addonTerminal.__pushToBuffer(str || ev.data);
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(this.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
}
} else if (typeof ev.data === 'string') {
displayData(ev.data);
} else {
addonTerminal.write(str || ev.data);
throw Error(`Cannot handle "${typeof ev.data}" websocket message.`);
}
};
/**
* Push data to buffer or write it in the terminal.
* This is used as a callback for FileReader.onload.
*
* @param str String decoded by FileReader.
* @param data The data of the EventMessage.
*/
function displayData(str?: string, data?: string): void {
if (buffered) {
addonTerminal.__pushToBuffer(str || data);
} else {
addonTerminal.write(str || data);
}
}
addonTerminal.__sendData = (data: string) => {
if (socket.readyState !== 1) {
return;
@@ -68,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"
]
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.weblinks",
"main": "weblinks.js",
"private": true
}
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true,
"preserveWatchOutput": true
},
"include": [
"**/*.ts",
"../../../typings/xterm.d.ts"
]
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as webLinks from './webLinks';
class MockTerminal {
public regex: RegExp;
public handler: (event: MouseEvent, uri: string) => void;
public options?: any;
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
this.regex = regex;
this.handler = handler;
this.options = options;
return 0;
}
}
describe('webLinks addon', () => {
describe('apply', () => {
it('should do register the `webLinksInit` method', () => {
webLinks.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
const match = row.match(term.regex);
const uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
});
+46
View File
@@ -0,0 +1,46 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal, ILinkMatcherOptions } from 'xterm';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
window.open(uri, '_blank');
}
/**
* Initialize the web links addon, registering the link matcher.
* @param term The terminal to use web links within.
* @param handler A custom handler to use.
* @param options Custom options to use, matchIndex will always be ignored.
*/
export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
webLinksInit(this, handler, options);
};
}
+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;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "xterm.winptyCompat",
"name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
+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"
]
}
+8 -9
View File
@@ -3,8 +3,6 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
@@ -38,22 +36,23 @@ import { Terminal } from 'xterm';
let zmodem;
export interface IZModemOptions {
export interface IZmodemOptions {
noTerminalWriteOutsideSession?: boolean;
}
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void {
function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
const term = this;
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
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)
);
@@ -71,7 +70,7 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
// 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);
}
}
@@ -85,8 +84,8 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
}
export function apply(terminalConstructor: typeof Terminal): void {
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this);
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
}
@@ -6,10 +6,6 @@
import { assert } from 'chai';
import { CircularList } from './CircularList';
class TestCircularList<T> extends CircularList<T> {
public get array(): T[] { return this._array; }
}
describe('CircularList', () => {
describe('push', () => {
it('should push values onto the array', () => {
@@ -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)];
}
@@ -58,16 +58,6 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
this._length = newLength;
}
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
return (callbackfn: (value: T, index: number) => void) => {
let i = 0;
let length = this.length;
for (let i = 0; i < length; i++) {
callbackfn(this.get(i), i);
}
};
}
/**
* Gets the value at an index.
*
+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;
}
+44 -37
View File
@@ -4,13 +4,13 @@
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
import { C0 } from '../common/data/EscapeSequences';
enum Direction {
Up = 'A',
Down = 'B',
Right = 'C',
Left = 'D'
const enum Direction {
UP = 'A',
DOWN = 'B',
RIGHT = 'C',
LEFT = 'D'
}
export class AltClickHandler {
@@ -28,7 +28,7 @@ export class AltClickHandler {
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
const coordinates = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
@@ -36,16 +36,20 @@ export class AltClickHandler {
this._terminal.cols,
this._terminal.rows,
false
).map((coordinate: number) => {
return coordinate - 1;
});
);
if (coordinates) {
[this._endCol, this._endRow] = coordinates.map((coordinate: number) => {
return coordinate - 1;
});
}
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey) {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.send(this._arrowSequences());
}
}
@@ -56,9 +60,13 @@ export class AltClickHandler {
* then moves to requested col.
*/
private _arrowSequences(): string {
return this._resetStartingRow() +
this._moveToRequestedRow() +
this._moveToRequestedCol();
// The alt buffer should try to navigate between rows
if (!this._terminal.buffer.hasScrollback) {
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
}
// Only move horizontally for the normal buffer
return this._moveHorizontallyOnly();
}
/**
@@ -67,17 +75,13 @@ export class AltClickHandler {
* positioning.
*/
private _resetStartingRow(): string {
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let endRow = this._endRow;
if (this._moveToRequestedRow().length === 0) {
return '';
} else {
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.Left));
}
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.LEFT));
}
/**
@@ -85,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()));
}
@@ -104,15 +108,20 @@ 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,
direction === Direction.Right
direction === Direction.RIGHT
).length, this._sequence(direction));
}
private _moveHorizontallyOnly(): string {
const direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
/**
* Utility functions
*/
@@ -123,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++;
@@ -173,10 +182,9 @@ export class AltClickHandler {
startRow <= this._endRow) || // down/right or same y/right
(this._startCol >= this._endCol &&
startRow < this._endRow)) { // down/left or same y/left
return Direction.Right;
} else {
return Direction.Left;
return Direction.RIGHT;
}
return Direction.LEFT;
}
/**
@@ -184,10 +192,9 @@ export class AltClickHandler {
*/
private _verticalDirection(): Direction {
if (this._startRow > this._endRow) {
return Direction.Up;
} else {
return Direction.Down;
return Direction.UP;
}
return Direction.DOWN;
}
/**
+12 -7
View File
@@ -4,17 +4,22 @@
*/
import { assert } from 'chai';
import * as Terminal from '../Terminal';
import * as Clipboard from './Clipboard';
describe('evaluatePastedTextProcessing', () => {
it('should replace carriage return + line feed with line feed on windows', () => {
const pastedText = 'foo\r\nbar\r\n';
const processedText = Clipboard.prepareTextForTerminal(pastedText, false);
const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
it('should replace carriage return and/or line feed with carriage return', () => {
const pastedText = {
unix: 'foo\nbar\n',
windows: 'foo\r\nbar\r\n'
};
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\rbar\r');
const processedText = {
unix: Clipboard.prepareTextForTerminal(pastedText.unix),
windows: Clipboard.prepareTextForTerminal(pastedText.windows)
};
assert.equal(processedText.unix, 'foo\rbar\r');
assert.equal(processedText.windows, 'foo\rbar\r');
});
it('should bracket pasted text in bracketedPasteMode', () => {
const pastedText = 'foo bar';
+4 -7
View File
@@ -18,11 +18,8 @@ declare var window: IWindow;
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
if (isMSWindows) {
return text.replace(/\r?\n/g, '\r');
}
return text;
export function prepareTextForTerminal(text: string): string {
return text.replace(/\r?\n/g, '\r');
}
/**
@@ -61,8 +58,8 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
let text: string;
let dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
const dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, term.bracketedPasteMode);
term.handler(text);
term.textarea.value = '';
+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;
}
}
+34 -78
View File
@@ -4,12 +4,11 @@
*/
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal, ITerminalOptions } from '../Types';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1;
const DIM_OPACITY = 0.5;
import { CharData, ITerminal } from '../Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -21,7 +20,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _charAtlas: HTMLCanvasElement | ImageBitmap;
protected _charAtlas: BaseCharAtlas;
constructor(
private _container: HTMLElement,
@@ -50,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);
@@ -84,16 +83,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
result.then(bitmap => this._charAtlas = bitmap);
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
@@ -110,9 +104,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this.clearAll();
}
if (charSizeChanged) {
this._refreshCharAtlas(terminal, this._colors);
}
this._refreshCharAtlas(terminal, this._colors);
}
public abstract reset(terminal: ITerminal): void;
@@ -182,7 +174,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -202,7 +194,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
width * this._scaledCellWidth,
height * this._scaledCellHeight);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
@@ -222,7 +214,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param color The color of the character.
*/
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = this._getFont(terminal, false);
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'top';
this._clipRow(terminal, y);
this._ctx.fillText(
@@ -245,56 +237,19 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void {
let colorIndex = 0;
if (fg < 256) {
colorIndex = fg + 2;
} else {
// If default color and bold
if (bold && terminal.options.enableBold) {
colorIndex = 1;
}
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
fg += drawInBrightColor ? 8 : 0;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
if (!atlasDidDraw) {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
}
const isAscii = code < 256;
// A color is basic if it is one of the standard normal or bold weight
// colors of the characters held in the char atlas. Note that this excludes
// the normal weight _light_ color characters.
const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold);
const isDefaultColor = fg >= 256;
const isDefaultBackground = bg >= 256;
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
// Apply alpha to dim the character
if (dim) {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw the non-bold version of the same color if bold is not enabled
if (bold && !terminal.options.enableBold) {
// Ignore default color as it's not touched above
if (colorIndex > 1) {
colorIndex -= 8;
}
}
this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth,
colorIndex * charAtlasCellHeight,
charAtlasCellWidth,
this._scaledCharHeight,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop,
charAtlasCellWidth,
this._scaledCharHeight);
} else {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
// this._ctx.drawImage(this._charAtlas, 0, 0);
}
/**
@@ -308,18 +263,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
this._ctx.save();
this._ctx.font = this._getFont(terminal, bold);
this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground;
this._ctx.fillStyle = this._colors.foreground.css;
}
this._clipRow(terminal, y);
@@ -356,10 +311,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param terminal The terminal.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: ITerminal, isBold: boolean): string {
protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
}
}
-136
View File
@@ -1,136 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../Types';
import { IColorSet } from './Types';
import { isFirefox } from '../shared/utils/Browser';
import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator';
export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
fontWeight: string;
fontWeightBold: string;
scaledCharWidth: number;
scaledCharHeight: number;
allowTransparency: boolean;
colors: IColorSet;
}
interface ICharAtlasCacheEntry {
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
config: ICharAtlasConfig;
ownedBy: ITerminal[];
}
let charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.bitmap;
} else {
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.bitmap;
}
}
const canvasFactory = (width: number, height: number) => {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
return canvas;
};
const charAtlasConfig: ICharAtlasRequest = {
scaledCharWidth,
scaledCharHeight,
fontSize: terminal.options.fontSize,
fontFamily: terminal.options.fontFamily,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
background: colors.background,
foreground: colors.foreground,
ansiColors: colors.ansi,
devicePixelRatio: window.devicePixelRatio,
allowTransparency: terminal.options.allowTransparency
};
const newEntry: ICharAtlasCacheEntry = {
bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.bitmap;
}
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
+268 -250
View File
@@ -3,268 +3,286 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ColorManager } from './ColorManager';
describe('ColorManager', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: Window;
beforeEach(() => {
cm = new ColorManager();
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(<any>window).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
fillRect(): void { },
getImageData(): any {
return {data: [0, 0, 0, 0xFF]};
}
});
cm = new ColorManager(document, false);
});
describe('constructor', () => {
it('should fill all colors with values', () => {
for (let key in cm.colors) {
if (typeof key === 'string') {
for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi') {
// A #rrggbb or rgba(...)
assert.ok(cm.colors[key].length >= 7);
assert.ok((<any>cm.colors)[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
});
it('should fill 240 colors with expected values', () => {
assert.equal(cm.colors.ansi[16], '#000000');
assert.equal(cm.colors.ansi[17], '#00005f');
assert.equal(cm.colors.ansi[18], '#000087');
assert.equal(cm.colors.ansi[19], '#0000af');
assert.equal(cm.colors.ansi[20], '#0000d7');
assert.equal(cm.colors.ansi[21], '#0000ff');
assert.equal(cm.colors.ansi[22], '#005f00');
assert.equal(cm.colors.ansi[23], '#005f5f');
assert.equal(cm.colors.ansi[24], '#005f87');
assert.equal(cm.colors.ansi[25], '#005faf');
assert.equal(cm.colors.ansi[26], '#005fd7');
assert.equal(cm.colors.ansi[27], '#005fff');
assert.equal(cm.colors.ansi[28], '#008700');
assert.equal(cm.colors.ansi[29], '#00875f');
assert.equal(cm.colors.ansi[30], '#008787');
assert.equal(cm.colors.ansi[31], '#0087af');
assert.equal(cm.colors.ansi[32], '#0087d7');
assert.equal(cm.colors.ansi[33], '#0087ff');
assert.equal(cm.colors.ansi[34], '#00af00');
assert.equal(cm.colors.ansi[35], '#00af5f');
assert.equal(cm.colors.ansi[36], '#00af87');
assert.equal(cm.colors.ansi[37], '#00afaf');
assert.equal(cm.colors.ansi[38], '#00afd7');
assert.equal(cm.colors.ansi[39], '#00afff');
assert.equal(cm.colors.ansi[40], '#00d700');
assert.equal(cm.colors.ansi[41], '#00d75f');
assert.equal(cm.colors.ansi[42], '#00d787');
assert.equal(cm.colors.ansi[43], '#00d7af');
assert.equal(cm.colors.ansi[44], '#00d7d7');
assert.equal(cm.colors.ansi[45], '#00d7ff');
assert.equal(cm.colors.ansi[46], '#00ff00');
assert.equal(cm.colors.ansi[47], '#00ff5f');
assert.equal(cm.colors.ansi[48], '#00ff87');
assert.equal(cm.colors.ansi[49], '#00ffaf');
assert.equal(cm.colors.ansi[50], '#00ffd7');
assert.equal(cm.colors.ansi[51], '#00ffff');
assert.equal(cm.colors.ansi[52], '#5f0000');
assert.equal(cm.colors.ansi[53], '#5f005f');
assert.equal(cm.colors.ansi[54], '#5f0087');
assert.equal(cm.colors.ansi[55], '#5f00af');
assert.equal(cm.colors.ansi[56], '#5f00d7');
assert.equal(cm.colors.ansi[57], '#5f00ff');
assert.equal(cm.colors.ansi[58], '#5f5f00');
assert.equal(cm.colors.ansi[59], '#5f5f5f');
assert.equal(cm.colors.ansi[60], '#5f5f87');
assert.equal(cm.colors.ansi[61], '#5f5faf');
assert.equal(cm.colors.ansi[62], '#5f5fd7');
assert.equal(cm.colors.ansi[63], '#5f5fff');
assert.equal(cm.colors.ansi[64], '#5f8700');
assert.equal(cm.colors.ansi[65], '#5f875f');
assert.equal(cm.colors.ansi[66], '#5f8787');
assert.equal(cm.colors.ansi[67], '#5f87af');
assert.equal(cm.colors.ansi[68], '#5f87d7');
assert.equal(cm.colors.ansi[69], '#5f87ff');
assert.equal(cm.colors.ansi[70], '#5faf00');
assert.equal(cm.colors.ansi[71], '#5faf5f');
assert.equal(cm.colors.ansi[72], '#5faf87');
assert.equal(cm.colors.ansi[73], '#5fafaf');
assert.equal(cm.colors.ansi[74], '#5fafd7');
assert.equal(cm.colors.ansi[75], '#5fafff');
assert.equal(cm.colors.ansi[76], '#5fd700');
assert.equal(cm.colors.ansi[77], '#5fd75f');
assert.equal(cm.colors.ansi[78], '#5fd787');
assert.equal(cm.colors.ansi[79], '#5fd7af');
assert.equal(cm.colors.ansi[80], '#5fd7d7');
assert.equal(cm.colors.ansi[81], '#5fd7ff');
assert.equal(cm.colors.ansi[82], '#5fff00');
assert.equal(cm.colors.ansi[83], '#5fff5f');
assert.equal(cm.colors.ansi[84], '#5fff87');
assert.equal(cm.colors.ansi[85], '#5fffaf');
assert.equal(cm.colors.ansi[86], '#5fffd7');
assert.equal(cm.colors.ansi[87], '#5fffff');
assert.equal(cm.colors.ansi[88], '#870000');
assert.equal(cm.colors.ansi[89], '#87005f');
assert.equal(cm.colors.ansi[90], '#870087');
assert.equal(cm.colors.ansi[91], '#8700af');
assert.equal(cm.colors.ansi[92], '#8700d7');
assert.equal(cm.colors.ansi[93], '#8700ff');
assert.equal(cm.colors.ansi[94], '#875f00');
assert.equal(cm.colors.ansi[95], '#875f5f');
assert.equal(cm.colors.ansi[96], '#875f87');
assert.equal(cm.colors.ansi[97], '#875faf');
assert.equal(cm.colors.ansi[98], '#875fd7');
assert.equal(cm.colors.ansi[99], '#875fff');
assert.equal(cm.colors.ansi[100], '#878700');
assert.equal(cm.colors.ansi[101], '#87875f');
assert.equal(cm.colors.ansi[102], '#878787');
assert.equal(cm.colors.ansi[103], '#8787af');
assert.equal(cm.colors.ansi[104], '#8787d7');
assert.equal(cm.colors.ansi[105], '#8787ff');
assert.equal(cm.colors.ansi[106], '#87af00');
assert.equal(cm.colors.ansi[107], '#87af5f');
assert.equal(cm.colors.ansi[108], '#87af87');
assert.equal(cm.colors.ansi[109], '#87afaf');
assert.equal(cm.colors.ansi[110], '#87afd7');
assert.equal(cm.colors.ansi[111], '#87afff');
assert.equal(cm.colors.ansi[112], '#87d700');
assert.equal(cm.colors.ansi[113], '#87d75f');
assert.equal(cm.colors.ansi[114], '#87d787');
assert.equal(cm.colors.ansi[115], '#87d7af');
assert.equal(cm.colors.ansi[116], '#87d7d7');
assert.equal(cm.colors.ansi[117], '#87d7ff');
assert.equal(cm.colors.ansi[118], '#87ff00');
assert.equal(cm.colors.ansi[119], '#87ff5f');
assert.equal(cm.colors.ansi[120], '#87ff87');
assert.equal(cm.colors.ansi[121], '#87ffaf');
assert.equal(cm.colors.ansi[122], '#87ffd7');
assert.equal(cm.colors.ansi[123], '#87ffff');
assert.equal(cm.colors.ansi[124], '#af0000');
assert.equal(cm.colors.ansi[125], '#af005f');
assert.equal(cm.colors.ansi[126], '#af0087');
assert.equal(cm.colors.ansi[127], '#af00af');
assert.equal(cm.colors.ansi[128], '#af00d7');
assert.equal(cm.colors.ansi[129], '#af00ff');
assert.equal(cm.colors.ansi[130], '#af5f00');
assert.equal(cm.colors.ansi[131], '#af5f5f');
assert.equal(cm.colors.ansi[132], '#af5f87');
assert.equal(cm.colors.ansi[133], '#af5faf');
assert.equal(cm.colors.ansi[134], '#af5fd7');
assert.equal(cm.colors.ansi[135], '#af5fff');
assert.equal(cm.colors.ansi[136], '#af8700');
assert.equal(cm.colors.ansi[137], '#af875f');
assert.equal(cm.colors.ansi[138], '#af8787');
assert.equal(cm.colors.ansi[139], '#af87af');
assert.equal(cm.colors.ansi[140], '#af87d7');
assert.equal(cm.colors.ansi[141], '#af87ff');
assert.equal(cm.colors.ansi[142], '#afaf00');
assert.equal(cm.colors.ansi[143], '#afaf5f');
assert.equal(cm.colors.ansi[144], '#afaf87');
assert.equal(cm.colors.ansi[145], '#afafaf');
assert.equal(cm.colors.ansi[146], '#afafd7');
assert.equal(cm.colors.ansi[147], '#afafff');
assert.equal(cm.colors.ansi[148], '#afd700');
assert.equal(cm.colors.ansi[149], '#afd75f');
assert.equal(cm.colors.ansi[150], '#afd787');
assert.equal(cm.colors.ansi[151], '#afd7af');
assert.equal(cm.colors.ansi[152], '#afd7d7');
assert.equal(cm.colors.ansi[153], '#afd7ff');
assert.equal(cm.colors.ansi[154], '#afff00');
assert.equal(cm.colors.ansi[155], '#afff5f');
assert.equal(cm.colors.ansi[156], '#afff87');
assert.equal(cm.colors.ansi[157], '#afffaf');
assert.equal(cm.colors.ansi[158], '#afffd7');
assert.equal(cm.colors.ansi[159], '#afffff');
assert.equal(cm.colors.ansi[160], '#d70000');
assert.equal(cm.colors.ansi[161], '#d7005f');
assert.equal(cm.colors.ansi[162], '#d70087');
assert.equal(cm.colors.ansi[163], '#d700af');
assert.equal(cm.colors.ansi[164], '#d700d7');
assert.equal(cm.colors.ansi[165], '#d700ff');
assert.equal(cm.colors.ansi[166], '#d75f00');
assert.equal(cm.colors.ansi[167], '#d75f5f');
assert.equal(cm.colors.ansi[168], '#d75f87');
assert.equal(cm.colors.ansi[169], '#d75faf');
assert.equal(cm.colors.ansi[170], '#d75fd7');
assert.equal(cm.colors.ansi[171], '#d75fff');
assert.equal(cm.colors.ansi[172], '#d78700');
assert.equal(cm.colors.ansi[173], '#d7875f');
assert.equal(cm.colors.ansi[174], '#d78787');
assert.equal(cm.colors.ansi[175], '#d787af');
assert.equal(cm.colors.ansi[176], '#d787d7');
assert.equal(cm.colors.ansi[177], '#d787ff');
assert.equal(cm.colors.ansi[178], '#d7af00');
assert.equal(cm.colors.ansi[179], '#d7af5f');
assert.equal(cm.colors.ansi[180], '#d7af87');
assert.equal(cm.colors.ansi[181], '#d7afaf');
assert.equal(cm.colors.ansi[182], '#d7afd7');
assert.equal(cm.colors.ansi[183], '#d7afff');
assert.equal(cm.colors.ansi[184], '#d7d700');
assert.equal(cm.colors.ansi[185], '#d7d75f');
assert.equal(cm.colors.ansi[186], '#d7d787');
assert.equal(cm.colors.ansi[187], '#d7d7af');
assert.equal(cm.colors.ansi[188], '#d7d7d7');
assert.equal(cm.colors.ansi[189], '#d7d7ff');
assert.equal(cm.colors.ansi[190], '#d7ff00');
assert.equal(cm.colors.ansi[191], '#d7ff5f');
assert.equal(cm.colors.ansi[192], '#d7ff87');
assert.equal(cm.colors.ansi[193], '#d7ffaf');
assert.equal(cm.colors.ansi[194], '#d7ffd7');
assert.equal(cm.colors.ansi[195], '#d7ffff');
assert.equal(cm.colors.ansi[196], '#ff0000');
assert.equal(cm.colors.ansi[197], '#ff005f');
assert.equal(cm.colors.ansi[198], '#ff0087');
assert.equal(cm.colors.ansi[199], '#ff00af');
assert.equal(cm.colors.ansi[200], '#ff00d7');
assert.equal(cm.colors.ansi[201], '#ff00ff');
assert.equal(cm.colors.ansi[202], '#ff5f00');
assert.equal(cm.colors.ansi[203], '#ff5f5f');
assert.equal(cm.colors.ansi[204], '#ff5f87');
assert.equal(cm.colors.ansi[205], '#ff5faf');
assert.equal(cm.colors.ansi[206], '#ff5fd7');
assert.equal(cm.colors.ansi[207], '#ff5fff');
assert.equal(cm.colors.ansi[208], '#ff8700');
assert.equal(cm.colors.ansi[209], '#ff875f');
assert.equal(cm.colors.ansi[210], '#ff8787');
assert.equal(cm.colors.ansi[211], '#ff87af');
assert.equal(cm.colors.ansi[212], '#ff87d7');
assert.equal(cm.colors.ansi[213], '#ff87ff');
assert.equal(cm.colors.ansi[214], '#ffaf00');
assert.equal(cm.colors.ansi[215], '#ffaf5f');
assert.equal(cm.colors.ansi[216], '#ffaf87');
assert.equal(cm.colors.ansi[217], '#ffafaf');
assert.equal(cm.colors.ansi[218], '#ffafd7');
assert.equal(cm.colors.ansi[219], '#ffafff');
assert.equal(cm.colors.ansi[220], '#ffd700');
assert.equal(cm.colors.ansi[221], '#ffd75f');
assert.equal(cm.colors.ansi[222], '#ffd787');
assert.equal(cm.colors.ansi[223], '#ffd7af');
assert.equal(cm.colors.ansi[224], '#ffd7d7');
assert.equal(cm.colors.ansi[225], '#ffd7ff');
assert.equal(cm.colors.ansi[226], '#ffff00');
assert.equal(cm.colors.ansi[227], '#ffff5f');
assert.equal(cm.colors.ansi[228], '#ffff87');
assert.equal(cm.colors.ansi[229], '#ffffaf');
assert.equal(cm.colors.ansi[230], '#ffffd7');
assert.equal(cm.colors.ansi[231], '#ffffff');
assert.equal(cm.colors.ansi[232], '#080808');
assert.equal(cm.colors.ansi[233], '#121212');
assert.equal(cm.colors.ansi[234], '#1c1c1c');
assert.equal(cm.colors.ansi[235], '#262626');
assert.equal(cm.colors.ansi[236], '#303030');
assert.equal(cm.colors.ansi[237], '#3a3a3a');
assert.equal(cm.colors.ansi[238], '#444444');
assert.equal(cm.colors.ansi[239], '#4e4e4e');
assert.equal(cm.colors.ansi[240], '#585858');
assert.equal(cm.colors.ansi[241], '#626262');
assert.equal(cm.colors.ansi[242], '#6c6c6c');
assert.equal(cm.colors.ansi[243], '#767676');
assert.equal(cm.colors.ansi[244], '#808080');
assert.equal(cm.colors.ansi[245], '#8a8a8a');
assert.equal(cm.colors.ansi[246], '#949494');
assert.equal(cm.colors.ansi[247], '#9e9e9e');
assert.equal(cm.colors.ansi[248], '#a8a8a8');
assert.equal(cm.colors.ansi[249], '#b2b2b2');
assert.equal(cm.colors.ansi[250], '#bcbcbc');
assert.equal(cm.colors.ansi[251], '#c6c6c6');
assert.equal(cm.colors.ansi[252], '#d0d0d0');
assert.equal(cm.colors.ansi[253], '#dadada');
assert.equal(cm.colors.ansi[254], '#e4e4e4');
assert.equal(cm.colors.ansi[255], '#eeeeee');
assert.equal(cm.colors.ansi[16].css, '#000000');
assert.equal(cm.colors.ansi[17].css, '#00005f');
assert.equal(cm.colors.ansi[18].css, '#000087');
assert.equal(cm.colors.ansi[19].css, '#0000af');
assert.equal(cm.colors.ansi[20].css, '#0000d7');
assert.equal(cm.colors.ansi[21].css, '#0000ff');
assert.equal(cm.colors.ansi[22].css, '#005f00');
assert.equal(cm.colors.ansi[23].css, '#005f5f');
assert.equal(cm.colors.ansi[24].css, '#005f87');
assert.equal(cm.colors.ansi[25].css, '#005faf');
assert.equal(cm.colors.ansi[26].css, '#005fd7');
assert.equal(cm.colors.ansi[27].css, '#005fff');
assert.equal(cm.colors.ansi[28].css, '#008700');
assert.equal(cm.colors.ansi[29].css, '#00875f');
assert.equal(cm.colors.ansi[30].css, '#008787');
assert.equal(cm.colors.ansi[31].css, '#0087af');
assert.equal(cm.colors.ansi[32].css, '#0087d7');
assert.equal(cm.colors.ansi[33].css, '#0087ff');
assert.equal(cm.colors.ansi[34].css, '#00af00');
assert.equal(cm.colors.ansi[35].css, '#00af5f');
assert.equal(cm.colors.ansi[36].css, '#00af87');
assert.equal(cm.colors.ansi[37].css, '#00afaf');
assert.equal(cm.colors.ansi[38].css, '#00afd7');
assert.equal(cm.colors.ansi[39].css, '#00afff');
assert.equal(cm.colors.ansi[40].css, '#00d700');
assert.equal(cm.colors.ansi[41].css, '#00d75f');
assert.equal(cm.colors.ansi[42].css, '#00d787');
assert.equal(cm.colors.ansi[43].css, '#00d7af');
assert.equal(cm.colors.ansi[44].css, '#00d7d7');
assert.equal(cm.colors.ansi[45].css, '#00d7ff');
assert.equal(cm.colors.ansi[46].css, '#00ff00');
assert.equal(cm.colors.ansi[47].css, '#00ff5f');
assert.equal(cm.colors.ansi[48].css, '#00ff87');
assert.equal(cm.colors.ansi[49].css, '#00ffaf');
assert.equal(cm.colors.ansi[50].css, '#00ffd7');
assert.equal(cm.colors.ansi[51].css, '#00ffff');
assert.equal(cm.colors.ansi[52].css, '#5f0000');
assert.equal(cm.colors.ansi[53].css, '#5f005f');
assert.equal(cm.colors.ansi[54].css, '#5f0087');
assert.equal(cm.colors.ansi[55].css, '#5f00af');
assert.equal(cm.colors.ansi[56].css, '#5f00d7');
assert.equal(cm.colors.ansi[57].css, '#5f00ff');
assert.equal(cm.colors.ansi[58].css, '#5f5f00');
assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
assert.equal(cm.colors.ansi[60].css, '#5f5f87');
assert.equal(cm.colors.ansi[61].css, '#5f5faf');
assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
assert.equal(cm.colors.ansi[63].css, '#5f5fff');
assert.equal(cm.colors.ansi[64].css, '#5f8700');
assert.equal(cm.colors.ansi[65].css, '#5f875f');
assert.equal(cm.colors.ansi[66].css, '#5f8787');
assert.equal(cm.colors.ansi[67].css, '#5f87af');
assert.equal(cm.colors.ansi[68].css, '#5f87d7');
assert.equal(cm.colors.ansi[69].css, '#5f87ff');
assert.equal(cm.colors.ansi[70].css, '#5faf00');
assert.equal(cm.colors.ansi[71].css, '#5faf5f');
assert.equal(cm.colors.ansi[72].css, '#5faf87');
assert.equal(cm.colors.ansi[73].css, '#5fafaf');
assert.equal(cm.colors.ansi[74].css, '#5fafd7');
assert.equal(cm.colors.ansi[75].css, '#5fafff');
assert.equal(cm.colors.ansi[76].css, '#5fd700');
assert.equal(cm.colors.ansi[77].css, '#5fd75f');
assert.equal(cm.colors.ansi[78].css, '#5fd787');
assert.equal(cm.colors.ansi[79].css, '#5fd7af');
assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
assert.equal(cm.colors.ansi[82].css, '#5fff00');
assert.equal(cm.colors.ansi[83].css, '#5fff5f');
assert.equal(cm.colors.ansi[84].css, '#5fff87');
assert.equal(cm.colors.ansi[85].css, '#5fffaf');
assert.equal(cm.colors.ansi[86].css, '#5fffd7');
assert.equal(cm.colors.ansi[87].css, '#5fffff');
assert.equal(cm.colors.ansi[88].css, '#870000');
assert.equal(cm.colors.ansi[89].css, '#87005f');
assert.equal(cm.colors.ansi[90].css, '#870087');
assert.equal(cm.colors.ansi[91].css, '#8700af');
assert.equal(cm.colors.ansi[92].css, '#8700d7');
assert.equal(cm.colors.ansi[93].css, '#8700ff');
assert.equal(cm.colors.ansi[94].css, '#875f00');
assert.equal(cm.colors.ansi[95].css, '#875f5f');
assert.equal(cm.colors.ansi[96].css, '#875f87');
assert.equal(cm.colors.ansi[97].css, '#875faf');
assert.equal(cm.colors.ansi[98].css, '#875fd7');
assert.equal(cm.colors.ansi[99].css, '#875fff');
assert.equal(cm.colors.ansi[100].css, '#878700');
assert.equal(cm.colors.ansi[101].css, '#87875f');
assert.equal(cm.colors.ansi[102].css, '#878787');
assert.equal(cm.colors.ansi[103].css, '#8787af');
assert.equal(cm.colors.ansi[104].css, '#8787d7');
assert.equal(cm.colors.ansi[105].css, '#8787ff');
assert.equal(cm.colors.ansi[106].css, '#87af00');
assert.equal(cm.colors.ansi[107].css, '#87af5f');
assert.equal(cm.colors.ansi[108].css, '#87af87');
assert.equal(cm.colors.ansi[109].css, '#87afaf');
assert.equal(cm.colors.ansi[110].css, '#87afd7');
assert.equal(cm.colors.ansi[111].css, '#87afff');
assert.equal(cm.colors.ansi[112].css, '#87d700');
assert.equal(cm.colors.ansi[113].css, '#87d75f');
assert.equal(cm.colors.ansi[114].css, '#87d787');
assert.equal(cm.colors.ansi[115].css, '#87d7af');
assert.equal(cm.colors.ansi[116].css, '#87d7d7');
assert.equal(cm.colors.ansi[117].css, '#87d7ff');
assert.equal(cm.colors.ansi[118].css, '#87ff00');
assert.equal(cm.colors.ansi[119].css, '#87ff5f');
assert.equal(cm.colors.ansi[120].css, '#87ff87');
assert.equal(cm.colors.ansi[121].css, '#87ffaf');
assert.equal(cm.colors.ansi[122].css, '#87ffd7');
assert.equal(cm.colors.ansi[123].css, '#87ffff');
assert.equal(cm.colors.ansi[124].css, '#af0000');
assert.equal(cm.colors.ansi[125].css, '#af005f');
assert.equal(cm.colors.ansi[126].css, '#af0087');
assert.equal(cm.colors.ansi[127].css, '#af00af');
assert.equal(cm.colors.ansi[128].css, '#af00d7');
assert.equal(cm.colors.ansi[129].css, '#af00ff');
assert.equal(cm.colors.ansi[130].css, '#af5f00');
assert.equal(cm.colors.ansi[131].css, '#af5f5f');
assert.equal(cm.colors.ansi[132].css, '#af5f87');
assert.equal(cm.colors.ansi[133].css, '#af5faf');
assert.equal(cm.colors.ansi[134].css, '#af5fd7');
assert.equal(cm.colors.ansi[135].css, '#af5fff');
assert.equal(cm.colors.ansi[136].css, '#af8700');
assert.equal(cm.colors.ansi[137].css, '#af875f');
assert.equal(cm.colors.ansi[138].css, '#af8787');
assert.equal(cm.colors.ansi[139].css, '#af87af');
assert.equal(cm.colors.ansi[140].css, '#af87d7');
assert.equal(cm.colors.ansi[141].css, '#af87ff');
assert.equal(cm.colors.ansi[142].css, '#afaf00');
assert.equal(cm.colors.ansi[143].css, '#afaf5f');
assert.equal(cm.colors.ansi[144].css, '#afaf87');
assert.equal(cm.colors.ansi[145].css, '#afafaf');
assert.equal(cm.colors.ansi[146].css, '#afafd7');
assert.equal(cm.colors.ansi[147].css, '#afafff');
assert.equal(cm.colors.ansi[148].css, '#afd700');
assert.equal(cm.colors.ansi[149].css, '#afd75f');
assert.equal(cm.colors.ansi[150].css, '#afd787');
assert.equal(cm.colors.ansi[151].css, '#afd7af');
assert.equal(cm.colors.ansi[152].css, '#afd7d7');
assert.equal(cm.colors.ansi[153].css, '#afd7ff');
assert.equal(cm.colors.ansi[154].css, '#afff00');
assert.equal(cm.colors.ansi[155].css, '#afff5f');
assert.equal(cm.colors.ansi[156].css, '#afff87');
assert.equal(cm.colors.ansi[157].css, '#afffaf');
assert.equal(cm.colors.ansi[158].css, '#afffd7');
assert.equal(cm.colors.ansi[159].css, '#afffff');
assert.equal(cm.colors.ansi[160].css, '#d70000');
assert.equal(cm.colors.ansi[161].css, '#d7005f');
assert.equal(cm.colors.ansi[162].css, '#d70087');
assert.equal(cm.colors.ansi[163].css, '#d700af');
assert.equal(cm.colors.ansi[164].css, '#d700d7');
assert.equal(cm.colors.ansi[165].css, '#d700ff');
assert.equal(cm.colors.ansi[166].css, '#d75f00');
assert.equal(cm.colors.ansi[167].css, '#d75f5f');
assert.equal(cm.colors.ansi[168].css, '#d75f87');
assert.equal(cm.colors.ansi[169].css, '#d75faf');
assert.equal(cm.colors.ansi[170].css, '#d75fd7');
assert.equal(cm.colors.ansi[171].css, '#d75fff');
assert.equal(cm.colors.ansi[172].css, '#d78700');
assert.equal(cm.colors.ansi[173].css, '#d7875f');
assert.equal(cm.colors.ansi[174].css, '#d78787');
assert.equal(cm.colors.ansi[175].css, '#d787af');
assert.equal(cm.colors.ansi[176].css, '#d787d7');
assert.equal(cm.colors.ansi[177].css, '#d787ff');
assert.equal(cm.colors.ansi[178].css, '#d7af00');
assert.equal(cm.colors.ansi[179].css, '#d7af5f');
assert.equal(cm.colors.ansi[180].css, '#d7af87');
assert.equal(cm.colors.ansi[181].css, '#d7afaf');
assert.equal(cm.colors.ansi[182].css, '#d7afd7');
assert.equal(cm.colors.ansi[183].css, '#d7afff');
assert.equal(cm.colors.ansi[184].css, '#d7d700');
assert.equal(cm.colors.ansi[185].css, '#d7d75f');
assert.equal(cm.colors.ansi[186].css, '#d7d787');
assert.equal(cm.colors.ansi[187].css, '#d7d7af');
assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
assert.equal(cm.colors.ansi[190].css, '#d7ff00');
assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
assert.equal(cm.colors.ansi[192].css, '#d7ff87');
assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
assert.equal(cm.colors.ansi[195].css, '#d7ffff');
assert.equal(cm.colors.ansi[196].css, '#ff0000');
assert.equal(cm.colors.ansi[197].css, '#ff005f');
assert.equal(cm.colors.ansi[198].css, '#ff0087');
assert.equal(cm.colors.ansi[199].css, '#ff00af');
assert.equal(cm.colors.ansi[200].css, '#ff00d7');
assert.equal(cm.colors.ansi[201].css, '#ff00ff');
assert.equal(cm.colors.ansi[202].css, '#ff5f00');
assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
assert.equal(cm.colors.ansi[204].css, '#ff5f87');
assert.equal(cm.colors.ansi[205].css, '#ff5faf');
assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
assert.equal(cm.colors.ansi[207].css, '#ff5fff');
assert.equal(cm.colors.ansi[208].css, '#ff8700');
assert.equal(cm.colors.ansi[209].css, '#ff875f');
assert.equal(cm.colors.ansi[210].css, '#ff8787');
assert.equal(cm.colors.ansi[211].css, '#ff87af');
assert.equal(cm.colors.ansi[212].css, '#ff87d7');
assert.equal(cm.colors.ansi[213].css, '#ff87ff');
assert.equal(cm.colors.ansi[214].css, '#ffaf00');
assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
assert.equal(cm.colors.ansi[216].css, '#ffaf87');
assert.equal(cm.colors.ansi[217].css, '#ffafaf');
assert.equal(cm.colors.ansi[218].css, '#ffafd7');
assert.equal(cm.colors.ansi[219].css, '#ffafff');
assert.equal(cm.colors.ansi[220].css, '#ffd700');
assert.equal(cm.colors.ansi[221].css, '#ffd75f');
assert.equal(cm.colors.ansi[222].css, '#ffd787');
assert.equal(cm.colors.ansi[223].css, '#ffd7af');
assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
assert.equal(cm.colors.ansi[226].css, '#ffff00');
assert.equal(cm.colors.ansi[227].css, '#ffff5f');
assert.equal(cm.colors.ansi[228].css, '#ffff87');
assert.equal(cm.colors.ansi[229].css, '#ffffaf');
assert.equal(cm.colors.ansi[230].css, '#ffffd7');
assert.equal(cm.colors.ansi[231].css, '#ffffff');
assert.equal(cm.colors.ansi[232].css, '#080808');
assert.equal(cm.colors.ansi[233].css, '#121212');
assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
assert.equal(cm.colors.ansi[235].css, '#262626');
assert.equal(cm.colors.ansi[236].css, '#303030');
assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
assert.equal(cm.colors.ansi[238].css, '#444444');
assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
assert.equal(cm.colors.ansi[240].css, '#585858');
assert.equal(cm.colors.ansi[241].css, '#626262');
assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
assert.equal(cm.colors.ansi[243].css, '#767676');
assert.equal(cm.colors.ansi[244].css, '#808080');
assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
assert.equal(cm.colors.ansi[246].css, '#949494');
assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
assert.equal(cm.colors.ansi[253].css, '#dadada');
assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
assert.equal(cm.colors.ansi[255].css, '#eeeeee');
});
});
@@ -276,20 +294,20 @@ describe('ColorManager', () => {
});
it('should set a partial set of colors, using the default if not present', () => {
assert.equal(cm.colors.background, '#000000');
assert.equal(cm.colors.foreground, '#ffffff');
assert.equal(cm.colors.background.css, '#000000');
assert.equal(cm.colors.foreground.css, '#ffffff');
cm.setTheme({
background: '#FF0000',
foreground: '#00FF00'
});
assert.equal(cm.colors.background, '#FF0000');
assert.equal(cm.colors.foreground, '#00FF00');
assert.equal(cm.colors.background.css, '#FF0000');
assert.equal(cm.colors.foreground.css, '#00FF00');
cm.setTheme({
background: '#0000FF'
});
assert.equal(cm.colors.background, '#0000FF');
assert.equal(cm.colors.background.css, '#0000FF');
// FG reverts back to default
assert.equal(cm.colors.foreground, '#ffffff');
assert.equal(cm.colors.foreground.css, '#ffffff');
});
});
});
+140 -63
View File
@@ -3,62 +3,79 @@
* @license MIT
*/
import { IColorSet, IColorManager } from './Types';
import { IColorManager } from './Types';
import { IColor, IColorSet } from '../shared/Types';
import { ITheme } from 'xterm';
const DEFAULT_FOREGROUND = '#ffffff';
const DEFAULT_BACKGROUND = '#000000';
const DEFAULT_CURSOR = '#ffffff';
const DEFAULT_CURSOR_ACCENT = '#000000';
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
export const DEFAULT_ANSI_COLORS = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
const DEFAULT_FOREGROUND = fromHex('#ffffff');
const DEFAULT_BACKGROUND = fromHex('#000000');
const DEFAULT_CURSOR = fromHex('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromHex('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF77
};
/**
* Fills an existing 16 length string with the remaining 240 ANSI colors.
* @param first16Colors The first 16 ANSI colors.
*/
function generate256Colors(first16Colors: string[]): string[] {
let colors = first16Colors.slice();
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
// and mutate that.
export const DEFAULT_ANSI_COLORS = (() => {
const colors = [
// dark:
fromHex('#2e3436'),
fromHex('#cc0000'),
fromHex('#4e9a06'),
fromHex('#c4a000'),
fromHex('#3465a4'),
fromHex('#75507b'),
fromHex('#06989a'),
fromHex('#d3d7cf'),
// bright:
fromHex('#555753'),
fromHex('#ef2929'),
fromHex('#8ae234'),
fromHex('#fce94f'),
fromHex('#729fcf'),
fromHex('#ad7fa8'),
fromHex('#34e2e2'),
fromHex('#eeeeec')
];
// 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 = toPaddedHex(v[(i / 36) % 6 | 0]);
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
const b = toPaddedHex(v[i % 6]);
colors.push(`#${r}${g}${b}`);
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`,
// Use >>> 0 to force a conversion to an unsigned int
rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = toPaddedHex(8 + i * 10);
colors.push(`#${c}${c}${c}`);
const c = 8 + i * 10;
const ch = toPaddedHex(c);
colors.push({
css: `#${ch}${ch}${ch}`,
rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0
});
}
return colors;
})();
function fromHex(css: string): IColor {
return {
css,
rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
};
}
function toPaddedHex(c: number): string {
let s = c.toString(16);
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
@@ -67,15 +84,23 @@ function toPaddedHex(c: number): string {
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
constructor() {
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
this._ctx = canvas.getContext('2d');
this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
ansi: DEFAULT_ANSI_COLORS.slice()
};
}
@@ -85,26 +110,78 @@ export class ColorManager implements IColorManager {
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = theme.background || DEFAULT_BACKGROUND;
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2];
this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3];
this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4];
this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5];
this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6];
this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7];
this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8];
this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9];
this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10];
this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11];
this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12];
this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13];
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
}
private _parseColor(
css: string,
fallback: IColor,
allowTransparency: boolean = this.allowTransparency
): IColor {
if (!css) {
return fallback;
}
// If parsing the value results in failure, then it must be ignored, and the attribute must
// retain its previous value.
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
this._ctx.fillStyle = this._litmusColor;
this._ctx.fillStyle = css;
if (typeof this._ctx.fillStyle !== 'string') {
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
return fallback;
}
this._ctx.fillRect(0, 0, 1, 1);
const data = this._ctx.getImageData(0, 0, 1, 1).data;
if (!allowTransparency && data[3] !== 0xFF) {
// Ideally we'd just ignore the alpha channel, but...
//
// Browsers may not give back exactly the same RGB values we put in, because most/all
// convert the color to a pre-multiplied representation. getImageData converts that back to
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
// on their own.
//
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
// into 0x00000000.
//
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
// pixels that have just been set using putImageData() might be returned to an equivalent
// getImageData() as different values."
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
//
// So let's just use the fallback color in this case instead.
console.warn(
`Color: ${css} is using transparency, but allowTransparency is false. ` +
`Using fallback ${fallback.css}.`
);
return fallback;
}
return {
css,
rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0
};
}
}
+18 -20
View File
@@ -3,11 +3,10 @@
* @license MIT
*/
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData, IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Types';
import { CharData, ITerminal } from '../Types';
interface ICursorState {
x: number;
@@ -26,7 +25,6 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'cursor', zIndex, true, colors);
@@ -35,7 +33,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
@@ -45,15 +43,15 @@ export class CursorRenderLayer extends BaseRenderLayer {
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
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 = {
x: null,
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
}
@@ -137,7 +135,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!terminal.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
this._ctx.restore();
this._state.x = terminal.buffer.x;
@@ -185,37 +183,37 @@ export class CursorRenderLayer extends BaseRenderLayer {
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
}
}
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillLeftLineAtCell(x, y);
this._ctx.restore();
}
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.fillStyle = this._colors.cursorAccent;
this._ctx.fillStyle = this._colors.cursorAccent.css;
this.fillCharTrueColor(terminal, charData, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor;
this._ctx.strokeStyle = this._colors.cursor.css;
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.restore();
}
@@ -237,7 +235,7 @@ class CursorBlinkStateManager {
constructor(
terminal: ITerminal,
private renderCallback: () => void
private _renderCallback: () => void
) {
this.isCursorVisible = true;
if (terminal.isFocused) {
@@ -272,7 +270,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
}
@@ -303,7 +301,7 @@ class CursorBlinkStateManager {
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
@@ -322,7 +320,7 @@ class CursorBlinkStateManager {
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
}, BLINK_INTERVAL);
+23 -10
View File
@@ -3,11 +3,9 @@
* @license MIT
*/
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, IBuffer, ICharMeasure, LinkHoverEventTypes } from '../Types';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: ILinkHoverEvent = null;
@@ -18,8 +16,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
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 = null;
}
@@ -30,14 +28,29 @@ export class LinkRenderLayer extends BaseRenderLayer {
private _clearCurrentLink(): void {
if (this._state) {
this.clearCells(this._state.x, this._state.y, this._state.length, 1);
this.clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
const middleRowCount = this._state.y2 - this._state.y1 - 1;
if (middleRowCount > 0) {
this.clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
}
this.clearCells(0, this._state.y2, this._state.x2, 1);
this._state = null;
}
}
private _onLinkHover(e: ILinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length);
this._ctx.fillStyle = this._colors.foreground.css;
if (e.y1 === e.y2) {
// Single line link
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
} else {
// Multi-line link
this.fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
for (let y = e.y1 + 1; y < e.y2; y++) {
this.fillBottomLineAtCells(0, y, e.cols);
}
this.fillBottomLineAtCells(0, e.y2, e.x2);
}
this._state = e;
}
+20 -48
View File
@@ -3,23 +3,20 @@
* @license MIT
*/
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
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 {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
private _renderDebouncer: RenderDebouncer;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
@@ -32,13 +29,14 @@ export class Renderer extends EventEmitter implements IRenderer {
constructor(private _terminal: ITerminal, theme: ITheme) {
super();
this.colorManager = new ColorManager();
const allowTransparency = this._terminal.options.allowTransparency;
this.colorManager = new ColorManager(document, allowTransparency);
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._terminal.options.allowTransparency),
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
@@ -61,14 +59,17 @@ export class Renderer extends EventEmitter implements IRenderer {
this._updateDimensions();
this.onOptionsChanged();
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() });
}
}
@@ -84,7 +85,7 @@ export class Renderer extends EventEmitter implements IRenderer {
// and the terminal needs to refreshed
if (this._devicePixelRatio !== devicePixelRatio) {
this._devicePixelRatio = devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
}
@@ -106,12 +107,12 @@ export class Renderer extends EventEmitter implements IRenderer {
return this.colorManager.colors;
}
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
// Force a refresh
if (this._isPaused) {
@@ -121,7 +122,7 @@ export class Renderer extends EventEmitter implements IRenderer {
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth + this._terminal.viewport.scrollBarWidth}px`;
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', {
@@ -131,7 +132,7 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
public onBlur(): void {
@@ -142,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 {
@@ -151,6 +152,7 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onOptionsChanged(): void {
this.colorManager.allowTransparency = this._terminal.options.allowTransparency;
this._runOperation(l => l.onOptionsChanged(this._terminal));
}
@@ -172,47 +174,19 @@ export class Renderer extends EventEmitter implements IRenderer {
* @param {number} start The start row.
* @param {number} end The end row.
*/
public queueRefresh(start: number, end: number): void {
public refreshRows(start: number, end: number): void {
if (this._isPaused) {
this._needsFullRefresh = true;
return;
}
this._refreshRowsQueue.push({ start: start, end: end });
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
}
this._renderDebouncer.refresh(start, end);
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
private _refreshLoop(): void {
let start;
let end;
if (this._refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this._terminal.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this._refreshRowsQueue[0].start;
end = this._refreshRowsQueue[0].end;
for (let i = 1; i < this._refreshRowsQueue.length; i++) {
if (this._refreshRowsQueue[i].start < start) {
start = this._refreshRowsQueue[i].start;
}
if (this._refreshRowsQueue[i].end > end) {
end = this._refreshRowsQueue[i].end;
}
}
}
this._refreshRowsQueue = [];
this._refreshAnimationFrame = null;
// Render
start = Math.max(start, 0);
end = Math.min(end, this._terminal.rows - 1);
private _renderRows(start: number, end: number): void {
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
}
@@ -275,7 +249,5 @@ export class Renderer extends EventEmitter implements IRenderer {
// differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
}
}
+61 -31
View File
@@ -3,45 +3,50 @@
* @license MIT
*/
import { IBuffer, ICharMeasure, ITerminal } from '../Types';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
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, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
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;
}
@@ -64,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;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
this._ctx.fillStyle = this._colors.selection.css;
// 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];
}
}
+133 -106
View File
@@ -5,16 +5,17 @@
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CharData, IBuffer, ICharMeasure, ITerminal } from '../Types';
import { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { BaseRenderLayer } from './BaseRenderLayer';
/**
* This CharData looks like a null character, which will forc a clear and render
* when the character changes (a regular space ' ' character may not as it's
* drawn state is a cleared cell).
*/
const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
export class TextRenderLayer extends BaseRenderLayer {
private _state: GridCache<CharData>;
@@ -27,11 +28,11 @@ export class TextRenderLayer extends BaseRenderLayer {
this._state = new GridCache<CharData>();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Clear the character width cache if the font or width has changed
const terminalFont = this._getFont(terminal, false);
const terminalFont = this._getFont(terminal, false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
@@ -47,21 +48,24 @@ export class TextRenderLayer extends BaseRenderLayer {
this.clearAll();
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
for (let y = startRow; y <= endRow; y++) {
private _forEachCell(
terminal: ITerminal,
firstRow: number,
lastRow: number,
callback: (
code: number,
char: string,
width: number,
x: number,
y: number,
fg: number,
bg: number,
flags: number
) => void
): void {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row);
this.clearCells(0, y, terminal.cols, 1);
// for (let x = 0; x < terminal.cols; x++) {
// this._state.cache[x][y] = null;
// }
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
@@ -72,51 +76,12 @@ export class TextRenderLayer extends BaseRenderLayer {
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (width === 0) {
// this._state.cache[x][y] = null;
continue;
}
// If the character is a space and the character to the left is an
// overlapping character, skip the character and allow the overlapping
// char to take full control over this character's cell.
if (code === 32 /*' '*/) {
if (x > 0) {
const previousChar: CharData = line[x - 1];
if (this._isOverlapping(previousChar)) {
continue;
}
}
}
// Skip rendering if the character is identical
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) {
// // Skip render, contents are identical
// this._state.cache[x][y] = charData;
// continue;
// }
// Clear the old character was not a space with the default background
// const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE);
// if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) {
// this._clearChar(x, y);
// }
// this._state.cache[x][y] = charData;
const flags = attr >> 18;
let bg = attr & 0x1ff;
// Skip rendering if the character is invisible
const isDefaultBackground = bg >= 256;
const isInvisible = flags & FLAGS.INVISIBLE;
const isInverted = flags & FLAGS.INVERSE;
if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) {
continue;
}
// If the character is an overlapping char and the character to the right is a
// space, take ownership of the cell to the right.
if (width !== 0 && this._isOverlapping(charData)) {
if (this._isOverlapping(charData)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
@@ -134,10 +99,12 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (isInverted) {
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
@@ -149,47 +116,107 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
// Clear the cell next to this character if it's wide
if (width === 2) {
// this.clearCells(x + 1, y, 1, 1);
}
// Draw background
if (bg < 256) {
this._ctx.save();
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]);
this.fillCells(x, y, width, 1);
this._ctx.restore();
}
this._ctx.save();
if (flags & FLAGS.BOLD) {
this._ctx.font = this._getFont(terminal, true);
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
} else {
this._ctx.fillStyle = this._colors.foreground;
}
this.fillBottomLineAtCells(x, y);
}
this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM));
this._ctx.restore();
callback(code, char, width, x, y, fg, bg, flags);
}
}
}
/**
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
* same color together to reduce draw calls.
*/
private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void {
const ctx = this._ctx;
const cols = terminal.cols;
let startX: number = 0;
let startY: number = 0;
let prevFillStyle: string | null = null;
ctx.save();
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
if (bg === INVERTED_DEFAULT_COLOR) {
nextFillStyle = this._colors.foreground.css;
} else if (bg < 256) {
nextFillStyle = this._colors.ansi[bg].css;
}
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
// don't need to draw anything.
startX = x;
startY = y;
} if (y !== startY) {
// our row changed, draw the previous row
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, cols - startX, 1);
startX = x;
startY = y;
} else if (prevFillStyle !== nextFillStyle) {
// our color changed, draw the previous characters in this row
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, x - startX, 1);
startX = x;
startY = y;
}
prevFillStyle = nextFillStyle;
});
// flush the last color we encountered
if (prevFillStyle !== null) {
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, cols - startX, 1);
}
ctx.restore();
}
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
if (flags & FLAGS.UNDERLINE) {
this._ctx.save();
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
this.drawChar(
terminal, char, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
);
});
}
public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
if (this._charAtlas) {
this._charAtlas.beginFrame();
}
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
this._drawBackground(terminal, firstRow, lastRow);
this._drawForeground(terminal, firstRow, lastRow);
}
public onOptionsChanged(terminal: ITerminal): void {
this.setTransparency(terminal, terminal.options.allowTransparency);
}
@@ -238,13 +265,13 @@ export class TextRenderLayer extends BaseRenderLayer {
* @param x The column of the char.
* @param y The row of the char.
*/
private _clearChar(x: number, y: number): void {
let colsToClear = 1;
// Clear the adjacent character if it was wide
const state = this._state.cache[x][y];
if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
colsToClear = 2;
}
this.clearCells(x, y, colsToClear, 1);
}
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
+18 -17
View File
@@ -4,49 +4,50 @@
*/
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
* Flags used to render terminal text properly.
*/
export enum FLAGS {
export const enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16,
DIM = 32
DIM = 32,
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, didCharSizeChange: boolean): 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;
queueRefresh(start: number, end: number): void;
refreshRows(start: number, end: number): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
// TODO: We should probably rewrite the imports for IColorSet, but there's a lot of them
export { IColorSet };
export interface IRenderDimensions {
scaledCharWidth: number;
@@ -98,12 +99,12 @@ 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.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
resize(terminal: ITerminal, dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from './Types';
export default abstract class BaseCharAtlas {
private _didWarmUp: boolean = false;
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
protected _doWarmUp(): void { }
/**
* Called when we start drawing a new frame.
*
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
* of BaseRenderLayer.
*/
public beginFrame(): void { }
/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
public abstract draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import BaseCharAtlas from './BaseCharAtlas';
import DynamicCharAtlas from './DynamicCharAtlas';
import NoneCharAtlas from './NoneCharAtlas';
import StaticCharAtlas from './StaticCharAtlas';
const charAtlasImplementations = {
'none': NoneCharAtlas,
'static': StaticCharAtlas,
'dynamic': DynamicCharAtlas
};
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: ITerminal[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(
terminal: ITerminal,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas](
document,
newConfig
),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
* @param terminal The terminal to remove.
*/
export function removeTerminalFromCache(terminal: ITerminal): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only terminal
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}

Some files were not shown because too many files have changed in this diff Show More