Compare commits

...
400 Commits
Author SHA1 Message Date
Daniel Imms bd0d267d97 Merge pull request #2605 from Tyriar/r43
v4.3.0
2019-12-05 12:40:34 -08:00
Daniel Imms c9babb9477 v4.3.0 2019-12-05 12:34:36 -08:00
Daniel Imms 17cb610ef1 Merge pull request #2602 from Tyriar/fix_dom
Fix minimumContrastRatio on dom/truecolor
2019-12-04 10:59:01 -08:00
Daniel Imms 543b3d6c9f Fix minimumContrastRatio on dom/truecolor
Fixes #2593
2019-12-04 10:54:17 -08:00
Daniel Imms 8b5c48fe87 Merge pull request #2560 from ivanwonder/reverse-transparency
force alpha to 1 when using background color as inverted foreground color
2019-11-25 15:10:59 -08:00
Daniel Imms 1ff1809ab7 Support opaque inverse background in webgl, add test 2019-11-25 15:06:28 -08:00
Daniel Imms ef05a6a053 Draw transparent background in WebGL renderer
Part of #2252
2019-11-25 14:33:12 -08:00
Daniel Imms bceddfbc28 Cover inverse non-black uncached case in canvas renderer 2019-11-25 14:22:03 -08:00
Daniel Imms 4cd8b22a59 Use toRgba helper 2019-11-25 11:58:26 -08:00
Daniel Imms 1a5c3f9364 Properly set non-black transparency backgrounds
Before it was always set to 0 as the resolved color was pulled from
an ImageData
2019-11-25 11:57:52 -08:00
Daniel Imms cbb8dc7780 Merge remote-tracking branch 'ups/master' into pr/ivanwonder/2560 2019-11-25 11:14:03 -08:00
Daniel Imms 3b1479a02b Merge pull request #2585 from Tyriar/unused
Remove unused imports/functions
2019-11-25 09:24:20 -08:00
Daniel Imms afe6cb6b86 Remove unused imports/functions 2019-11-25 09:12:39 -08:00
Daniel Imms 2173a5399d Merge pull request #2583 from xtermjs/Tyriar-patch-1
Include ` in word separators
2019-11-25 07:50:10 -08:00
Daniel Imms 25f2e66222 Include ` in word separators
See microsoft/vscode#84483
2019-11-25 07:36:06 -08:00
Daniel Imms 397d053e06 Merge pull request #2582 from Tyriar/2581_cursor
Mark before and after cursor rows dirty when changed
2019-11-24 11:22:47 -08:00
Daniel Imms 217bc24235 Mark before and after cursor rows dirty when changed
The DOM renderer renders the cursor as part of the content, not on a
separate layer, so it needs to be marked dirty to draw.

Fixes #2581
2019-11-24 11:15:28 -08:00
Daniel Imms 4d2afce5f2 Merge pull request #2580 from Tyriar/puppeteer_release
Skip puppeteer download during release job
2019-11-24 09:47:18 -08:00
Daniel Imms 2da5a7a26b Skip puppeteer download during release job 2019-11-24 09:42:33 -08:00
Daniel Imms fffbec7294 Merge pull request #2579 from Tyriar/publish_fix
Fix publish script for dot separated beta
2019-11-24 09:35:44 -08:00
Daniel Imms c60b822f4b Fix publish script for dot separated beta 2019-11-24 09:34:29 -08:00
Daniel Imms 2882ae76ef Merge pull request #2578 from Tyriar/85048
Don't throw when rendering before atlas is set
2019-11-24 09:26:13 -08:00
Daniel Imms 53fb1ba54d Don't throw when rendering before atlas is set
This fixes a problem when loading the webgl addon before the terminal
is attached.

See microsoft/vscode#85048
2019-11-24 09:22:07 -08:00
Daniel Imms 40d8b606a3 Merge pull request #2577 from Tyriar/beta_dot
Add a . before beta and beta version
2019-11-22 19:38:47 -08:00
Daniel Imms 2f1f2f8785 Add a . before beta and beta version
Fixes #2576
2019-11-22 19:37:55 -08:00
Daniel Imms 1300b7a6b7 Merge pull request #2575 from Tyriar/webgl_exception
Prevent atlas missing exception when opened before attach
2019-11-22 16:06:30 -08:00
Daniel Imms 0bd0f3a364 Prevent atlas missing exception when opened before attach
See microsoft/vscode#85048
2019-11-22 16:00:25 -08:00
Daniel Imms c0da45113f Merge pull request #2574 from Tyriar/question
Direct question issues to Stack Overflow
2019-11-22 09:39:03 -08:00
Daniel Imms 93487d4fb3 Direct question issues to Stack Overflow 2019-11-22 09:34:06 -08:00
Daniel Imms 70babeacb6 Merge pull request #2569 from Tyriar/layers6
Move WindowsMode to common/
2019-11-15 10:27:55 -08:00
Daniel Imms 12e9dce6c7 Merge pull request #2563 from Tyriar/322_minimum_contrast
Implement minimum contrast ratio
2019-11-15 10:24:35 -08:00
Daniel Imms 943928c4d1 Move WindowsMode to common/
Part of #1507
2019-11-14 22:26:26 -08:00
Daniel Imms 5aa3157672 Return inverse colors correctly
This probably wasn't causing a bug since the opposite values were
being returned, it was wrong and made the code confusing though.
2019-11-14 22:12:36 -08:00
Daniel Imms e5f4e2ff8f Support min contrast ratio in canvas renderer too: 2019-11-14 22:07:50 -08:00
Daniel Imms c95337a742 Remove unneeded params from webgl funcs 2019-11-14 21:57:15 -08:00
Daniel Imms 023fab9095 Remove unused import 2019-11-14 20:34:01 -08:00
Daniel Imms cc9f649e3a Merge branch 'master' into 322_minimum_contrast 2019-11-14 20:14:17 -08:00
Daniel Imms be9700878b Have inverse bold respect drawBoldTextInBrightColors 2019-11-14 20:02:40 -08:00
Daniel Imms 24dc87af2c Only enter updateBackgounds condition if cell changed
We want to check when the background changed or when the foreground
changed (and either the old or new value was inverse)
2019-11-14 19:46:24 -08:00
jerch c99a096d52 Merge pull request #2566 from jerch/binary_event
onBinary event
2019-11-14 22:39:32 +01:00
Jörg Breitbart 57fe84508c apply empty report rule to all encodings; comments added 2019-11-14 22:29:46 +01:00
Jörg Breitbart f1f103850c fix tests; fix attach addon 2019-11-14 00:48:16 +01:00
Jörg Breitbart 00d626b6cc add onBinary to interfaces 2019-11-13 23:57:51 +01:00
Jörg Breitbart b780abaf3b add onBinary on CoreService, use for DEFAULT mouse reports 2019-11-13 23:46:01 +01:00
Daniel Imms 7652cd8af3 Use hash notation over rgb() for dom colors 2019-11-13 06:59:50 -08:00
Daniel Imms 579d74ee33 jsdoc rgbRelativeLuminance2 2019-11-13 06:53:29 -08:00
Daniel Imms 2c959dfb2e Keep a working AttributeData in WebglCharAtlas 2019-11-13 06:52:17 -08:00
Daniel Imms 20b0a95bb0 Add tests for ensureContrastRatioRgba 2019-11-12 11:05:58 -08:00
Daniel Imms db090c70de Add unit tests for ColorContrastCache 2019-11-12 10:21:23 -08:00
Daniel Imms d1f7d2ef6a Tweak readme to call out min contrast ratio 2019-11-12 10:10:55 -08:00
Daniel Imms 56d12fe846 Fix color manager unit test 2019-11-12 10:09:32 -08:00
Daniel Imms b0c3606207 Add example values to .d.ts 2019-11-12 10:03:38 -08:00
Daniel Imms 032185f8b0 Link webgl directly to browser/Color 2019-11-12 10:00:40 -08:00
Daniel Imms b3f439af09 Remove unused imports 2019-11-12 09:55:09 -08:00
Daniel Imms 647cff53db Lint 2019-11-12 09:53:42 -08:00
Daniel Imms 3b0042fd46 Remove log 2019-11-12 09:46:07 -08:00
Daniel Imms 8a0122f482 Add contrast caching to webgl 2019-11-12 09:42:55 -08:00
Daniel Imms faaa07be68 Add contrast caching for DOM renderer 2019-11-12 09:33:50 -08:00
Daniel Imms c2474626f0 Clear setting as part of run 2019-11-12 09:07:08 -08:00
Daniel Imms f5774fdc70 Merge branch 'master' into 322_minimum_contrast 2019-11-12 08:53:25 -08:00
Daniel Imms ca5cee7e86 Fix contrast tests 2019-11-12 08:53:06 -08:00
Daniel Imms 5ee9bcf2b5 Remove .only 2019-11-11 23:23:33 -08:00
Daniel Imms 6626f9628d Use ceil in both 2019-11-11 23:14:27 -08:00
Daniel Imms 2fc1791dde Fix floor/ceil in luminance methods 2019-11-11 23:11:36 -08:00
Daniel Imms 414eea2a2c Merge pull request #2562 from AndrewSverdrup/change-codevolve-to-next-tech
Update Codevolve to Next Tech
2019-11-11 22:56:35 -08:00
Daniel Imms 9b139a744b Add test for min contrast on white bg 2019-11-11 22:55:08 -08:00
Daniel Imms 531409c2c7 Support minimum contrast ratio in webgl 2019-11-11 22:47:26 -08:00
Daniel Imms 2283ea45a6 Refresh rows after ratio changes 2019-11-11 22:18:39 -08:00
Daniel Imms db775afd70 Support drawBoldTextInBrightColors in webgl again
This broke as part of this PR
2019-11-11 21:35:51 -08:00
Daniel Imms b1bcb899e0 Fix inverse in webgl, add tests 2019-11-11 21:29:02 -08:00
Daniel Imms a6dcb21f1c Fix tests 2019-11-11 19:30:36 -08:00
Daniel Imms 9cd66e72bb Add tests for relative luminance 2019-11-11 19:14:35 -08:00
Daniel Imms e46ff9ec85 Add minimumContrastRatio to DOM renderer
Part of #322
2019-11-11 19:03:12 -08:00
ivanwonder fd868044fd add a helper to make color opaque 2019-11-12 10:06:51 +08:00
Andrew Sverdrup dfc1664d6e Update Codevolve to Next Tech
We changed our name several months back.
2019-11-11 13:23:59 -08:00
Daniel Imms e911656306 Merge pull request #2561 from xtermjs/Tyriar-patch-1
Prefer non-relative imports
2019-11-11 10:54:15 -08:00
Daniel Imms 25f9487239 Prefer non-relative imports
Fixes some auto imports in VS Code, see microsoft/TypeScript#32144
2019-11-11 10:24:54 -08:00
Daniel Imms 339d4d6a73 Merge pull request #2559 from xtermjs/Tyriar-patch-1
Generate coverage report for pipelines
2019-11-10 21:43:41 -08:00
Daniel Imms 94afd1fa38 Generate coverage report for pipelines 2019-11-10 21:38:43 -08:00
ivanwonder 5bc80fdf67 force alpha to 1 when using background color as inverted foreground color. 2019-11-11 11:01:32 +08:00
Daniel Imms 03fe1677b6 Merge pull request #2558 from Tyriar/force_build
Whitespace to force a webgl release
2019-11-10 12:07:01 -08:00
Daniel Imms 6b7e29ea32 Whitespace to force a webgl release 2019-11-10 12:06:20 -08:00
Daniel Imms 9eeb5aac32 Merge pull request #2557 from Tyriar/fix_release
Correct import in Color.ts
2019-11-10 11:49:12 -08:00
Daniel Imms 450a626218 Correct import in Color.ts 2019-11-10 11:48:32 -08:00
Daniel Imms a8ff27ceb9 Merge pull request #2552 from Tyriar/webgl_compat_attr
True color support in WebGL renderer
2019-11-10 11:37:26 -08:00
Daniel Imms e89d79b879 Remove unused code 2019-11-10 11:33:41 -08:00
Daniel Imms 734bd8c96a Factor out toRgba function and test 2019-11-10 11:30:48 -08:00
Daniel Imms 2da77dd5f8 Use opaque selection in webgl, fix selection default rgba 2019-11-10 11:23:34 -08:00
Daniel Imms 51c77568a8 Add tests for Color functions 2019-11-10 11:17:36 -08:00
Daniel Imms 53b0149764 Add selectionOpaque and factor out color into new file 2019-11-10 11:11:26 -08:00
Daniel Imms 2b2bd91e2e Add background true color tests 2019-11-10 10:41:30 -08:00
Daniel Imms 350ca8d5fb Organize tests 2019-11-10 10:39:56 -08:00
Daniel Imms 753ad30dbc Add true color fg tests for webgl 2019-11-10 10:38:23 -08:00
Daniel Imms 8bf837795f Add tests for webgl colors 16-255 2019-11-10 10:29:50 -08:00
Daniel 01628362f5 Support selection color 2019-11-10 01:19:27 -08:00
Daniel 811307562b Use actual default color for rectangle renderer 2019-11-10 00:59:11 -08:00
Daniel Imms 98fe63051d Merge branch 'master' into webgl_compat_attr 2019-11-10 00:53:42 -08:00
Daniel Imms adc631d192 Merge pull request #2553 from Tyriar/retry
Add retries to onRender test
2019-11-10 00:53:36 -08:00
Daniel 2cde011397 Add retries to onRender test
I've seen this flaky locally and in CI
2019-11-10 00:49:10 -08:00
Daniel b56e29a35b Import clean up 2019-11-10 00:39:46 -08:00
Daniel 149cc76803 Avoid extra object creation for foreground fill 2019-11-10 00:30:58 -08:00
Daniel b13e9066bd Use a switch for color mode conditions 2019-11-10 00:29:50 -08:00
Daniel a2da8f9fbb Fix true color glyphs not having background cleared 2019-11-10 00:23:08 -08:00
Daniel 3773b22439 Support true color... mostly 2019-11-09 23:41:00 -08:00
Daniel 39f0e17edf Consolidate constants, fix rectangle rendering 2019-11-09 23:05:39 -08:00
Daniel 2b5d229aab Reduce size of model to 3 by removing attr 2019-11-09 22:56:42 -08:00
Daniel a2bd0e8eaa Remove attr compat from webgl 2019-11-09 22:50:22 -08:00
Daniel 9dd2ccb1ad Fix selection color to set correct mode 2019-11-09 22:48:28 -08:00
Daniel e15476704c Adopt new [bg][fg] map in char cache 2019-11-09 22:43:25 -08:00
Daniel 8f0f8bf827 Remove cache reliance on flags 2019-11-09 22:31:10 -08:00
Daniel e72ee3c58d Move webgl drawing over from compat
Still need to tackle glyph key
2019-11-09 22:28:59 -08:00
Daniel 8a6e0fca07 Move parts over to CellData 2019-11-09 21:15:07 -08:00
Daniel Imms 87d2015793 Merge pull request #2551 from Tyriar/puppeteer_slowmo
Make integration tests very fast by removing slowMo
2019-11-09 19:14:31 -08:00
Daniel Imms 40b3766abc Fix import that got lost in merge 2019-11-09 19:05:20 -08:00
Daniel Imms 0c69b68b4c Merge branch 'master' into puppeteer_slowmo 2019-11-09 19:02:55 -08:00
Daniel Imms 641b5f867b Merge pull request #2548 from Tyriar/2254_webgl_dispose
Implement webgl renderer dispose
2019-11-09 19:01:52 -08:00
Daniel Imms 057d0a16f2 Ensure link hover exists before hovering 2019-11-09 18:54:24 -08:00
Daniel Imms 669622108c Wait for all elements to be rendered in web links 2019-11-09 18:48:09 -08:00
Daniel Imms 971e69f505 Reduce flakiness 2019-11-09 18:43:25 -08:00
Daniel Imms b0640b7705 Remove slowMo param everywhere 2019-11-09 18:28:50 -08:00
Daniel Imms 7ff6e4f493 Fix flaky terminal api tests 2019-11-09 18:28:24 -08:00
Daniel Imms 34e703e472 Fix most of Terminal.api timing issues 2019-11-09 17:29:32 -08:00
Daniel Imms 17677568f9 Fix timing issues in mouse tracking tests 2019-11-09 14:16:57 -08:00
Daniel Imms 2aaa997d39 Fix timing issues in input handler 2019-11-09 14:15:16 -08:00
Daniel Imms 74f04ae92e Fix timing issues in char width test 2019-11-09 14:12:46 -08:00
Daniel Imms b0c0f8701e Fix timing issues in webglrenderer 2019-11-09 14:08:37 -08:00
Daniel Imms d3bf013ceb Fix timing issues in web links addon tests 2019-11-09 13:57:42 -08:00
Daniel Imms 840e56cf1e Fix Attach tests 2019-11-09 11:30:45 -08:00
Daniel Imms b8acdd6e67 Remove slowmo from puppeteer 2019-11-09 11:15:15 -08:00
Daniel Imms 60bda1bbd3 Fix tests hanging 2019-11-09 11:04:55 -08:00
Daniel Imms e4b50d505c Merge pull request #2550 from Tyriar/2549_refresh_options
Refresh rows on options change for all renderers
2019-11-09 10:37:28 -08:00
Daniel Imms 22f4342194 Move options row refresh into render service 2019-11-09 10:07:24 -08:00
Daniel Imms 8adbed84b0 webgl: Refresh rows on options change
Fixes #2549
2019-11-09 09:34:40 -08:00
Daniel Imms d09ac41f75 After tests wait for browser close 2019-11-09 09:29:04 -08:00
Daniel Imms 2aec4e6d29 Add a test for removing canvases 2019-11-09 09:27:30 -08:00
Daniel Imms bcd4c766d6 Implement webgl renderer dispose
Fixes #2254
2019-11-09 09:09:59 -08:00
Daniel Imms b68a4d27ab Merge pull request #2547 from Tyriar/layering5
Move remaining renderer parts into browser
2019-11-07 21:42:53 -08:00
Daniel Imms 939dcca5bb Merge branch 'master' into layering5 2019-11-07 21:22:06 -08:00
Daniel Imms 3bf149bbc5 Fix webgl cursor 2019-11-07 18:46:56 -08:00
Daniel Imms 3f7238d9dc Move Renderer to browser 2019-11-07 18:45:13 -08:00
Daniel Imms 873de2b127 Move cursor render layer to browser 2019-11-07 18:39:46 -08:00
Daniel Imms c204ab00e0 Move cursorHidden into core service 2019-11-07 18:22:18 -08:00
Daniel Imms 9d7935f8bd Move cursorState into core service 2019-11-07 18:21:06 -08:00
Daniel Imms 1e1038b2c7 Remove renderer circular dep on renderservice
Also fix super.dispose not being called correctly on RenderService
2019-11-07 18:09:40 -08:00
Daniel Imms 509d0e7019 Remove some ITerminal usage from Renderer 2019-11-07 17:44:03 -08:00
Daniel Imms 82e18d7aaa Move DomRenderer to browser, fix strict errors 2019-11-07 17:41:29 -08:00
Daniel Imms 7c39217392 Break DomRenderer dependency on ITerminal
Part of #1507
2019-11-07 17:35:22 -08:00
Daniel Imms dd4eb7bdec Merge pull request #2532 from glneto/2444
#2444: Fix wrapped lines search
2019-11-07 16:49:24 -08:00
Daniel Imms e84469d140 Merge pull request #2546 from Tyriar/polish_pipelines
Reduce duplicate in pipeline, cache in release job
2019-11-07 16:48:29 -08:00
Daniel Imms 1d63be0506 Reduce duplicate in pipeline, cache in release job 2019-11-07 13:52:39 -08:00
Daniel Imms fce7a67c8e Remove type jsdoc 2019-11-07 13:39:42 -08:00
Daniel Imms 94ce037eb8 Add integration tests for regression in #2444 2019-11-07 13:00:43 -08:00
Daniel Imms aad32e9943 Merge branch 'master' into 2444 2019-11-07 12:20:33 -08:00
Daniel Imms 16409da517 Merge pull request #2541 from Tyriar/2539
Skip puppeteer download on unit test runs, fix windows tests, polish pipeline
2019-11-06 11:52:45 -08:00
Daniel Imms 0878294964 Skip Terminal2 tests on Windows 2019-11-06 11:24:34 -08:00
Daniel Imms 31afe5b305 test-coverage -> test-unit-coverage 2019-11-06 11:23:42 -08:00
Daniel Imms fe2f1ea4df Get Windows unit tests running again
Fixes #2543
2019-11-06 11:20:52 -08:00
Daniel Imms 48b20e3200 Don't run through nyc on mac and Windows 2019-11-06 11:08:47 -08:00
Daniel Imms 030a8bda80 Set coverage lines threshold to 60% 2019-11-06 11:08:39 -08:00
Daniel Imms d079775690 Merge coverage check into regular unit test check
Fixes #2542
2019-11-06 11:04:37 -08:00
Daniel Imms d4dbdac73f Don't cache integration test modules 2019-11-06 10:53:42 -08:00
Daniel Imms fc0568074f Cache node modules with puppeteer 2019-11-06 10:37:00 -08:00
Daniel Imms 0630baaf02 Don't cache on integration tests 2019-11-05 12:50:37 -08:00
Daniel Imms c974b273d4 Install puppeteer explicitly 2019-11-05 12:41:03 -08:00
Daniel Imms d2164584d5 Fix error in yml 2019-11-05 11:56:44 -08:00
Daniel Imms 2960d5df53 New cache key 2019-11-05 11:55:06 -08:00
Daniel Imms 5411f60f44 Try cache node_modules 2019-11-05 11:54:32 -08:00
Daniel Imms 86264d4763 Specify yarn cache folder 2019-11-05 11:42:35 -08:00
Daniel Imms 70500e31ac Add yarn cache caching 2019-11-05 11:36:54 -08:00
Daniel Imms 3dca46619c Skip puppeteer download on unit test runs
Fixes #2539
2019-11-05 11:33:39 -08:00
Daniel Imms 7f598a3675 Merge pull request #2540 from Tyriar/ts37
Update TS to 3.7 and adopt optional chaining
2019-11-05 11:22:11 -08:00
Daniel Imms 843d5ff53c Adopt optional chaining 2019-11-05 11:09:47 -08:00
Daniel Imms 267fbc7f5e typescript@3.7 and fix errors 2019-11-05 10:54:02 -08:00
Daniel Imms d259cd7332 Merge pull request #2536 from Tyriar/parse_rows
Only refresh rows that changed during parsing
2019-11-04 11:31:50 -08:00
Daniel Imms 1afffba1d8 Only refresh rows that changed during parsing
Fixes #2534
2019-11-04 11:00:19 -08:00
Geraldo Neto 9358718b5f 2444: Fix no-else-return lint 2019-11-02 19:33:21 -03:00
Geraldo Neto 8667237525 Merge branch '2444' of https://github.com/glneto/xterm.js into 2444 2019-11-02 19:22:09 -03:00
Geraldo Neto 9d50d106e3 2444: Return after calculating startCol in reverse search 2019-11-02 19:21:58 -03:00
Geraldo Neto d53215480a Merge branch 'master' into 2444 2019-11-02 19:14:01 -03:00
Geraldo Neto 1f4780a71a #2444: Fix wrapped lines search 2019-11-02 18:58:04 -03:00
Daniel Imms bc9649bfbc Merge pull request #2527 from xtermjs/Tyriar-patch-1
Change to tracking lines in code coverage
2019-10-31 12:23:07 -07:00
Daniel Imms fbb9ef9f7f Change to tracking lines in code coverage 2019-10-31 11:59:04 -07:00
Daniel Imms e8153d929d Merge pull request #2525 from Tyriar/az
Polish pipeline config and add coverage reporting
2019-10-30 21:18:06 -07:00
Daniel 08b10ba651 Add note about unit test coverage in contributing 2019-10-30 21:15:23 -07:00
Daniel 39c0fdb54a Remove report directory 2019-10-30 20:31:40 -07:00
Daniel a450bd52d7 Generate and publish coverage report to pipelines 2019-10-30 20:18:32 -07:00
Daniel 79f3a7343c Don't wrap for single line scripts 2019-10-30 20:07:41 -07:00
Daniel 6c2b34a5c8 Use YarnInstaller 2019-10-30 20:03:53 -07:00
Daniel Imms 80e44f4978 Merge pull request #2518 from Tyriar/2407_toggle_addons
Support check boxes in demo to toggle addons
2019-10-29 14:49:23 -07:00
Daniel Imms 5b555bacc0 Merge pull request #2521 from alberto/patch-1
Fix typos
2019-10-28 07:28:32 -07:00
alberto cb1cf3cd11 Fix grammar 2019-10-27 22:50:22 +01:00
alberto b880cd3ad3 Fix punctuation mark 2019-10-27 22:43:26 +01:00
alberto ffe2d66aff Remove duplicated punctuation 2019-10-27 22:38:55 +01:00
alberto fe2ac04b85 Fix grammar 2019-10-27 22:34:54 +01:00
alberto 8d0fdc8731 Fix grammar 2019-10-27 22:29:36 +01:00
alberto dca1e0b935 Fix another typo 2019-10-27 22:18:11 +01:00
alberto 56d3366831 Fix typo 2019-10-27 22:08:42 +01:00
Daniel Imms 719fa55dcf Merge pull request #2517 from revanthmahesh/dispose-eventemitters
Support disposing of EventEmitters
2019-10-26 21:52:50 -07:00
Daniel Imms f3bbcb35e4 Clean up 2019-10-26 21:52:11 -07:00
Revanth Mahesh d0e05eb795 Support disposing of EventEmitters 2019-10-27 10:11:43 +05:30
Daniel 2f18edbcac Remove old webgl enable button 2019-10-26 18:12:56 -07:00
Daniel 486fc714b3 Support checkboxes in demo to toggle addons
Fixes #2407
2019-10-26 18:12:20 -07:00
jerch edc09572b5 Merge pull request #2516 from jerch/addon_build
conditional `yarn` for addons
2019-10-26 17:32:08 +02:00
Jörg Breitbart 5b3a093f13 stick to prepare to avoid re-running yarn for addons everytime during build 2019-10-26 17:20:35 +02:00
Jörg Breitbart 03435a9f4d use prebuild instead of postinstall 2019-10-25 22:11:51 +02:00
jerchandDaniel Imms 04f828bc1a Update bin/install-addons.js
Co-Authored-By: Daniel Imms <tyriar@tyriar.com>
2019-10-25 21:46:49 +02:00
Jörg Breitbart d733ce3e63 add copyright note 2019-10-25 19:27:29 +02:00
Jörg Breitbart 6f8cbc56eb skip folder w'o package.json; restrict linter to src folders 2019-10-25 18:07:32 +02:00
Jörg Breitbart 25c18e8e07 conditional build for addons 2019-10-25 17:29:38 +02:00
Daniel Imms f30c38f773 Merge pull request #2515 from Tyriar/r420
v4.2.0
2019-10-25 05:50:45 -07:00
Daniel bfe11b7081 v4.2.0 2019-10-25 05:38:17 -07:00
Daniel Imms a800dd3aab Merge pull request #2514 from Tyriar/scroll
Apply scrollSensitivity on fast scroll too
2019-10-25 05:32:39 -07:00
Daniel a5a0779aa9 Apply scrollSensitivity on fast scroll too
Fixes #2513
2019-10-25 05:22:03 -07:00
Daniel Imms 4fe5a5a0bb Merge pull request #2512 from Tyriar/refine_range
Refine IViewportRange API to use 'cursor positions' for x value
2019-10-25 04:48:51 -07:00
Daniel Imms 974c721f66 Merge pull request #2511 from Tyriar/marker_doc
Clarify marker API docs
2019-10-25 04:45:06 -07:00
Daniel 851fb7ae38 Refine IViewportRange API to use 'cursor positions' for x value
Fixes #2484
2019-10-25 04:43:34 -07:00
jerch 1c72a66567 Merge pull request #2507 from jerch/remove_obsolete_mouseencodings
remove URXVT and UTF8 mouse encoding from codebase
2019-10-25 13:37:00 +02:00
jerch 6f8919ab93 Merge branch 'master' into remove_obsolete_mouseencodings 2019-10-25 13:20:20 +02:00
Daniel dac2da05bf Clarify marker API docs 2019-10-25 04:13:03 -07:00
Daniel Imms 7487d61fe2 Merge pull request #2510 from Tyriar/line_one
Set disposed marker line to -1
2019-10-25 04:08:45 -07:00
Daniel 67e5974eee Set disposed marker line to -1
Another indicator that it's disposed/invalid
2019-10-25 04:04:17 -07:00
jerch 21c3b3243d Merge branch 'master' into remove_obsolete_mouseencodings 2019-10-25 12:53:40 +02:00
Daniel Imms 4bd721c749 Merge pull request #2505 from Tyriar/lgtm_line
Fix lgtm suppression comment line
2019-10-25 03:49:48 -07:00
Jörg Breitbart ee2c1f71ef fix typo in debug msg 2019-10-25 12:43:34 +02:00
Jörg Breitbart a721e4884d Merge branch 'remove_obsolete_mouseencodings' of github.com:jerch/xterm.js into remove_obsolete_mouseencodings 2019-10-25 12:42:17 +02:00
Jörg Breitbart 31be1ebc53 remove DECSET 1005 and 1015 from InputHandler 2019-10-25 12:41:31 +02:00
jerch be59c74eae Merge branch 'master' into remove_obsolete_mouseencodings 2019-10-25 12:02:18 +02:00
jerch c1a0b26150 Merge pull request #2508 from jerch/fix_2423
limit parse buffer size
2019-10-25 12:01:51 +02:00
Jörg Breitbart 13ddaf8f98 limit parse buffer size 2019-10-24 22:32:10 +02:00
jerch 6dc6748876 Merge branch 'master' into remove_obsolete_mouseencodings 2019-10-24 22:07:41 +02:00
jerch 527379d343 Merge pull request #2360 from jerch/left_right_scroll
Support column sequences SL (scroll left), SR (scroll right), DECIC (insert columns) and DECDC (delete columns).
2019-10-24 22:03:30 +02:00
Jörg Breitbart 5ea016d537 remove URXVT and UTF8 encoding from codebase 2019-10-24 21:57:22 +02:00
jerch bb424bd86f Merge branch 'master' into left_right_scroll 2019-10-24 21:46:02 +02:00
Daniel Imms a6f4855f66 Merge pull request #2506 from Tyriar/mp3_sound
Use mp3 audio instead of wav
2019-10-24 12:05:39 -07:00
Daniel Imms fe08f34918 Use mp3 audio instead of wav
wav needs ffmpeg which vscode does not ship with, mp3 is best for compat
2019-10-24 12:04:45 -07:00
Jörg Breitbart 68d6d5576c force screen update for altered lines 2019-10-24 21:01:59 +02:00
Jörg Breitbart a4d561d3b9 xterm testfiles for SL/SR/DECIC/DECDC with DECSTBM 2019-10-24 20:36:01 +02:00
Jörg Breitbart c223524f77 respect DECSTBM 2019-10-24 19:27:35 +02:00
Daniel Imms f8fa962ec7 Fix lgtm suppression comment line 2019-10-24 09:35:12 -07:00
jerch 2a46f5f147 Merge branch 'master' into left_right_scroll 2019-10-24 18:31:51 +02:00
Daniel Imms 080188c468 Merge pull request #2504 from jerch/fix_2495
fix typo in TextDecoder
2019-10-24 09:29:42 -07:00
jerch 1392abb6eb Merge branch 'master' into left_right_scroll 2019-10-24 18:07:35 +02:00
Jörg Breitbart 30d3d46027 make linter happy 2019-10-24 18:04:44 +02:00
Jörg Breitbart 6d620166b4 fix typo in decoder and add a test case 2019-10-24 17:52:03 +02:00
Daniel Imms 276463e95e Merge pull request #2501 from xtermjs/Tyriar-patch-1
Remove double space in dts
2019-10-23 11:37:30 -07:00
Daniel Imms 84e4cf7e9c Remove double space in dts 2019-10-23 11:37:17 -07:00
Daniel Imms d9f58c0dbb Merge pull request #2491 from miggs125/issue#2456
Prevent search result from being deselected if it is the only result
2019-10-22 07:32:26 -07:00
Daniel Imms 3e753f1370 Merge branch 'master' into issue#2456 2019-10-22 07:17:47 -07:00
Daniel Imms 616d341e8c Avoid initializing to undefined 2019-10-22 07:17:37 -07:00
Daniel Imms 15392c6c87 Merge pull request #2493 from leomoty/forbid-only
Forbid only in mocha
2019-10-22 07:15:17 -07:00
Daniel Imms 5e2001f4b5 Space out if 2019-10-22 07:14:32 -07:00
Daniel Imms 5194098f12 Merge pull request #2497 from Tyriar/warn_not_throw
Warn don't throw when open is called on unattached element
2019-10-22 07:02:30 -07:00
Daniel Imms 3ce552cdec Warn don't throw when open is called on unattached element
Related microsoft/vscode#83016
2019-10-22 06:56:05 -07:00
Miguel R 720c8c4623 Merge branch 'master' into issue#2456 2019-10-22 04:14:14 +00:00
Miguel Roncancio 243874c644 change comment 2019-10-21 23:51:47 -04:00
Miguel Roncancio 5d547bffb0 change return value, import ISelectionPosition 2019-10-21 23:49:28 -04:00
Miguel Roncancio 495f97cf95 change return value, import ISelectionPosition 2019-10-21 23:39:01 -04:00
Leonardo Bressan Motyczka 5c2479944e Ensure test-unit fails fast if .only present 2019-10-22 03:00:40 +00:00
Leonardo Bressan Motyczka 51ef0d0680 Update test-unit to allow double dash args 2019-10-22 03:00:18 +00:00
Daniel Imms 2b39887a37 Merge pull request #2492 from jmbockhorst/publicViewportRange
Expose IViewportRange in the public API
2019-10-21 19:18:03 -07:00
Daniel Imms 7c82f9c7d4 Merge pull request #2496 from Tyriar/lgtm_alerts
Fix alerts on lgtm.com
2019-10-21 19:11:23 -07:00
Daniel Imms 1dbe482fcb Suppress server alerts 2019-10-21 19:03:53 -07:00
Daniel Imms cfd158cbff Remove unused imports 2019-10-21 18:48:49 -07:00
Daniel Imms 9826f3a14c Remove unneeded defensive check 2019-10-21 18:47:43 -07:00
Daniel Imms 6f7c91d0cd Remove useless local var assignment 2019-10-21 18:47:18 -07:00
Daniel Imms bcdebe7648 Remove duplicate switch case
Guessing this one was the result of a bad merge?
2019-10-21 18:46:26 -07:00
Daniel Imms 09b244ed2f Fix NaN comparison 2019-10-21 18:44:03 -07:00
Leonardo Bressan Motyczka 7576e8c202 test-api: Fail fast if .only is used 2019-10-21 22:27:25 -03:00
Jon Bockhorst 7b752be68a Expose IViewportRange in the public API 2019-10-21 20:24:03 -05:00
Daniel Imms d02086e5c6 Add test to cover search multiple times 2019-10-21 15:23:59 -07:00
Daniel Imms 5c172c74b2 Merge branch 'master' into issue#2456 2019-10-21 14:34:10 -07:00
Daniel Imms d29fc6d5de Merge pull request #2489 from Tyriar/test_fiix
Remove .only from fit test
2019-10-21 14:31:10 -07:00
miguel 449ede1d57 Prevent search result from being deselected if it is the only result
remove leading whitespace
prevent single search term deselection in findPrevious
2019-10-21 15:58:44 -04:00
Daniel Imms fc10816615 Remove .only from fit test 2019-10-21 11:27:01 -07:00
Daniel Imms 631107698e Merge pull request #2483 from leomoty/fit-tests
Introduce FitAddon tests
2019-10-18 12:13:34 -07:00
Daniel Imms 33d58ec3d2 Merge pull request #2485 from eamodio/feature/scroll-sensitivity
Adds scrollSensitivity option for scrolling speed
2019-10-18 11:42:44 -07:00
Eric Amodio f445bc763b Adds scrollSensitivity option for scrolling speed 2019-10-18 14:30:46 -04:00
Daniel Imms f8a62a23e5 Speed tests up by avoiding page loads 2019-10-18 11:23:11 -07:00
Leonardo Bressan Motyczka a92635b22c Introduce FitAddon tests 2019-10-18 14:53:09 -03:00
Leonardo Bressan Motyczka 44a4bd788f Adjust proposeDimensions to behave like resize 2019-10-18 14:48:56 -03:00
Daniel Imms eb252433ce Merge pull request #2470 from jmbockhorst/linkPosition
Support link position in hover tooltip callback
2019-10-18 08:47:16 -07:00
Daniel Imms 499d6c26f0 Merge branch 'master' into linkPosition 2019-10-17 12:38:14 -07:00
Daniel 8350804858 Adjust to use 1-based, note in API 2019-10-17 12:36:03 -07:00
Daniel Imms 9df547d746 Merge pull request #2482 from Tyriar/2452_website_update
Create a PR for website update after a release
2019-10-17 12:26:45 -07:00
Daniel Imms 7537adfb72 Merge branch 'master' into 2452_website_update 2019-10-17 12:00:13 -07:00
Daniel caf075dcb0 Create a PR for website update after a release 2019-10-17 11:54:32 -07:00
Jon Bockhorst 57581ca4ef Updated IViewportCellPosition wording 2019-10-17 12:40:32 -05:00
Daniel Imms a604389cc0 Merge pull request #2481 from Tyriar/2154_force_release_var
Allow builds to specify a force release var
2019-10-17 10:28:34 -07:00
Jon Bockhorst 08475eb495 Update API to use IViewportRange and IViewportCellPosition 2019-10-17 12:25:42 -05:00
Daniel Imms 91d1f97754 Merge pull request #2476 from Tyriar/2459_win_scroll
Fix scrollback going missing when increase size on Windows
2019-10-17 10:19:46 -07:00
Daniel Imms 990be060f2 Allow builds to specify a force release var
This will let us automate patch releases by queuing a manual build

Fixes #2154
2019-10-17 10:00:45 -07:00
Daniel Imms 29e53c9c11 Merge pull request #2477 from leomoty/devcontainer-hotfix
Allows to run all integration tests inside devcontainer
2019-10-17 09:19:25 -07:00
Daniel Imms da42738881 Merge pull request #2474 from glneto/master
#2463 Add comma as a default word separator
2019-10-17 09:07:06 -07:00
Geraldo Neto 9753a7d425 Merge branch 'master' into master 2019-10-16 20:16:56 -03:00
Leonardo Bressan Motyczka 07308485a4 Update MouseTracking puppeteer window size
This allows all tests to successfully run in devcontainer
2019-10-16 20:08:57 -03:00
Leonardo Bressan Motyczka 3d500ba802 Launch puppeteer without sandbox 2019-10-16 20:07:56 -03:00
Leonardo Bressan Motyczka e8dc563ccf Include puppeteer deps on devcontainer 2019-10-16 20:05:45 -03:00
Daniel Imms ee245230c9 Merge branch 'master' into 2459_win_scroll 2019-10-16 15:44:19 -07:00
Daniel Imms 843ea1b610 Fix scrollback going missing when increase size on Windows
This also forces the scroll bar to sync after a resize early, this may fix
other bugs.

Fixes #2459
2019-10-16 15:41:47 -07:00
Geraldo Neto 9f80ce25c2 Add : and ; to the word separator 2019-10-16 19:23:50 -03:00
Daniel Imms bd57e305e1 Merge pull request #2475 from Tyriar/vsc_82309
Protect against exception when clicking on mouse zones
2019-10-16 11:08:16 -07:00
Daniel Imms 740d261d7a Protect against exception when clicking on mouse zones
Still not sure how this happens but seems harmless to just not use the row as
wrapped here.

See microsoft/vscode#82309
2019-10-16 11:07:22 -07:00
Geraldo Neto 1bc246323c Add comma as a default word separator 2019-10-15 22:42:12 -03:00
Daniel Imms 91cbeecf52 Merge pull request #2473 from Tyriar/2471
Mark element and textarea APIs as | undefined
2019-10-15 16:41:59 -07:00
Daniel Imms cfe8022867 Mark element and textarea APIs as | undefined
Didn't fix all the issues in Terminal.ts as they will be covered in layering
refactor

Fixes #2471
2019-10-15 16:29:13 -07:00
Daniel Imms 99db619a5c Merge pull request #2375 from csurquin/feat/fast-scrolling
Support fast scrolling using a modifier key
2019-10-15 16:23:17 -07:00
Daniel Imms 4f8cda9f3a Remove bad whitespace 2019-10-15 16:22:41 -07:00
Daniel Imms 9fdb674f20 Use min of 1 for fast scroll 2019-10-15 16:22:09 -07:00
Daniel Imms e76b8f86b5 Pull modifier code into helper, apply to lines scrolled 2019-10-15 16:21:20 -07:00
Daniel Imms 0083c23b2d Merge branch 'master' into feat/fast-scrolling 2019-10-15 16:10:44 -07:00
Jon Bockhorst 040f9f49d9 Support link position in hover tooltip callback 2019-10-14 14:25:02 -05:00
Daniel Imms cbb79a77f9 Merge pull request #2467 from Tyriar/2387_dont_do_ctrl_arrow_on_mac
Don't use ctrl+up/down hack on macOS
2019-10-14 11:53:27 -07:00
Daniel Imms a97fe2df1c Merge pull request #2469 from leomoty/dispose-test
Add integration test for terminal dispose
2019-10-14 10:58:07 -07:00
Leonardo Bressan Motyczka ff13f74858 Add integration test for terminal dispose 2019-10-14 13:20:18 -03:00
Daniel Imms 3d6815ca6f Add tests for alt+up/down 2019-10-11 16:05:54 -07:00
Daniel Imms da11e31fe5 Don't use ctrl+up/down hack on macOS
Fixes #2387
2019-10-11 16:01:28 -07:00
Daniel Imms e224b865fd Merge pull request #2465 from syrusakbary/patch-1
Added WebAssembly.sh
2019-10-10 13:54:25 -07:00
Syrus Akbary 821a7afc01 Added WebAssembly.sh 2019-10-10 13:05:28 -07:00
Daniel Imms 84b56b7691 Merge pull request #2453 from Tyriar/2445_reveal_search
Reveal search results in line just below viewport
2019-10-08 07:05:21 -07:00
Daniel Imms 8288a26b49 Merge pull request #2455 from JeffreyCA/unfocused-cursor-style
Update unfocused cursor to match current cursor style
2019-10-08 06:43:03 -07:00
Daniel Imms d87bae374b Merge pull request #2454 from saisandeepvaddi/master
add ten hands to real-world uses list
2019-10-08 06:36:55 -07:00
JeffreyCA 7c982fc0ce Fix lint warnings 2019-10-07 22:51:03 -07:00
JeffreyCA 1eb6e37187 Refresh terminal even if blinking is enabled so cursor changes are immediately visible 2019-10-07 22:36:37 -07:00
JeffreyCA efbdbd3f3b Render non-block cursors as-is when terminal is unfocused 2019-10-07 22:25:47 -07:00
Sai Sandeep Vaddi 7fc57da85a add ten hands to real-world uses list 2019-10-07 23:45:03 -04:00
Daniel Imms 6f5e5215f2 Reveal search results in line just below viewport
Fixes #2445
2019-10-07 14:27:29 -07:00
Daniel Imms cad9477eef Merge pull request #2451 from Tyriar/1416_fit_infinity
Generalize verify integers check
2019-10-07 10:46:07 -07:00
Daniel Imms 4fde8db060 Merge pull request #2450 from Tyriar/1158_throw_bad_open
Throw when open is called on element not on DOM
2019-10-07 10:45:38 -07:00
Daniel Imms 724bcc3766 Generalize verify integers check
Fixes #1416
2019-10-07 10:28:51 -07:00
Daniel Imms 52ae8fc14b Throw when open is called on element not on DOM
Fixes #1158
2019-10-07 10:21:34 -07:00
Daniel Imms e14b1f75fe Merge pull request #2449 from Tyriar/release_41
Release v4.1.0
2019-10-07 09:54:48 -07:00
Daniel Imms da5bf975b4 Merge branch 'master' into release_41 2019-10-07 09:44:30 -07:00
Daniel Imms 22398c0927 v4.1.0 2019-10-07 09:42:20 -07:00
Daniel Imms 43b291112e Merge pull request #2448 from Tyriar/597_cmd_arrow
Don't handle cmd+arrow
2019-10-07 08:27:42 -07:00
Daniel Imms 87a1fca726 Merge pull request #2447 from Tyriar/ts36
Upgrade to typescript 3.6
2019-10-07 08:27:28 -07:00
Daniel Imms 52e211915e Don't handle cmd+arrow
Fixes #597
2019-10-07 08:02:58 -07:00
Daniel Imms 92c991132f Upgrade to typescript 3.6 2019-10-07 07:55:48 -07:00
jerch 89ac2834f1 Merge branch 'master' into left_right_scroll 2019-10-04 20:12:34 +02:00
Daniel Imms 358c09a108 Merge branch 'master' into feat/fast-scrolling 2019-09-28 09:30:23 -07:00
Daniel Imms ff790236c1 Merge pull request #2442 from Tyriar/2441_windows_demo
Fix demo on Windows
2019-09-28 09:29:47 -07:00
Daniel Imms 9014a27d7d Fix demo on Windows
Fixes #2441
2019-09-28 09:25:41 -07:00
Daniel Imms 5236d4aabf Merge branch 'master' into feat/fast-scrolling 2019-09-28 08:51:28 -07:00
Daniel Imms ea922fe548 Merge pull request #2422 from jerch/write_enhancement
write callback support
2019-09-25 17:19:42 -07:00
Daniel Imms 22b06cada7 Merge pull request #2425 from Tyriar/constant_key_board
Use more C0 constants in Keyboard
2019-09-25 17:06:17 -07:00
Daniel Imms 5a65286c54 Merge branch 'master' into write_enhancement 2019-09-25 17:03:00 -07:00
Daniel Imms 0e8883ac99 Strongly type addSocketListener 2019-09-25 17:00:31 -07:00
Daniel Imms 5cb0bcea0f Merge pull request #2437 from kafeg/patch-1
Update README.md
2019-09-24 08:18:38 -07:00
Vitaly Petrov 2fd24849b2 Update README.md 2019-09-24 17:46:12 +03:00
Daniel Imms b81711e3da Merge pull request #2434 from Tyriar/2433_npe
Fix NPE when open called twice (master)
2019-09-23 10:55:21 -07:00
Daniel Imms 0c740213e3 Merge branch 'master' into 2433_npe 2019-09-23 07:49:34 -07:00
Daniel Imms f3b7866a19 Fix NPE when open called twice
Fixes #2433
2019-09-23 07:47:02 -07:00
Jörg Breitbart d34d1f56cb simplify attach addon 2019-09-22 22:33:55 +02:00
Jörg Breitbart 322565807b Merge branch 'master' into write_enhancement 2019-09-22 20:59:28 +02:00
Jörg Breitbart 1178ef2a12 Merge branch 'write_enhancement' of github.com:jerch/xterm.js into write_enhancement 2019-09-22 20:57:26 +02:00
Jörg Breitbart 6aab756ccd integration callback tests 2019-09-22 20:57:00 +02:00
Jörg Breitbart d1bd603382 unit tests 2019-09-22 20:39:09 +02:00
Daniel Imms 191932cce2 Merge pull request #2431 from Tyriar/2117_tests
Add tests for IL, DL and DCH
2019-09-14 14:31:00 -07:00
Daniel Imms bc7ba7bd85 Simplify test 2019-09-14 12:57:41 -07:00
Daniel Imms 74ba282056 Add tests for DCH (CSI Ps P)
Part of #2117
2019-09-14 12:15:16 -07:00
Daniel Imms 96f944522e Add tests for DL (CSI Ps M)
Part of #2117
2019-09-14 12:05:00 -07:00
Daniel Imms bf209fb71f Add tests for IL (CSI Ps L)
Part of #2117
2019-09-14 11:53:42 -07:00
Daniel Imms 3b537c7e9f Improve api jsdoc 2019-09-13 09:32:38 -07:00
Daniel Imms ce39c0633c Merge pull request #2429 from Tyriar/update_addons
Use v4 for addon peer dependency and release
2019-09-13 09:18:11 -07:00
Daniel Imms aba4d76ec6 v0.2.1 of all addons 2019-09-13 09:16:55 -07:00
Daniel Imms 91734f7659 Set addon peer deps to v4
Fixes #2428
2019-09-13 09:16:22 -07:00
Jörg Breitbart 1ac3cf8fca use callback in paste integration test 2019-09-13 16:45:40 +02:00
Jörg Breitbart c2ec16a16c encapsulate deferred writing in WriteBuffer class 2019-09-13 16:31:28 +02:00
Jörg Breitbart e402772cf7 Merge branch 'master' into write_enhancement 2019-09-13 13:30:10 +02:00
Jörg Breitbart d9ba19ba9e remove write sync hack from Terminal.test.ts 2019-09-13 13:23:59 +02:00
Jörg Breitbart a95e451227 add params in doc 2019-09-13 13:19:33 +02:00
Jörg Breitbart fe74b5588d use write callback in benchmark test 2019-09-13 13:11:23 +02:00
Jörg Breitbart dcbc7e1014 remove writeln from core 2019-09-13 13:06:54 +02:00
Jörg Breitbart 50e552262a remove writeUtf8 from core 2019-09-13 13:00:47 +02:00
Jörg Breitbart 597757d0ee use write callbacks in addon integration tests 2019-09-13 12:44:13 +02:00
Daniel Imms 5d3313a8ca Merge pull request #2427 from Tyriar/dist_remove
Remove extra build in publish Release job
2019-09-12 15:33:17 -07:00
Daniel Imms 9d291d1d12 Remove extra build in publish Release job
We no longer publish a dist dir
2019-09-12 15:16:03 -07:00
Daniel Imms 7da361af9f Merge pull request #2424 from Tyriar/logging_data
Improve debug logging by printing character codes
2019-09-12 14:02:58 -07:00
Daniel Imms 761e437c69 Allow lazy evaluated log params 2019-09-12 13:38:30 -07:00
Daniel Imms fc4f345446 Don't transform incoming data
This would run for all data
2019-09-12 11:24:04 -07:00
Daniel Imms 0a4883b2b3 Use more C0 constants in Keyboard 2019-09-12 11:14:41 -07:00
Daniel Imms 293f54e8dd Improve debug logging by printing character codes 2019-09-12 11:06:22 -07:00
Jörg Breitbart 05fad38240 fix private access in integration tests 2019-09-12 13:58:52 +02:00
Jörg Breitbart 75ba717bd7 merge write methods, optional callback 2019-09-12 13:36:58 +02:00
jerch 19f0fc0aa9 Merge branch 'master' into left_right_scroll 2019-09-12 10:55:36 +02:00
Daniel Imms ffd27dfef0 Merge pull request #2419 from Tyriar/master_changes
Master changes for 4.0.1 fixes
2019-09-11 14:50:58 -07:00
Daniel Imms e042f25b8d Expose ctor at window.Terminal
Fixes #2415
2019-09-11 14:39:26 -07:00
Daniel Imms 7f9f757042 Fix web links addon support in Edge
regex.flags isn't supported on Edge

Fixes #2417
2019-09-11 14:39:11 -07:00
Daniel Imms 5e1de3b062 Merge branch 'master' into feat/fast-scrolling 2019-09-07 13:49:42 -07:00
jerch 54739d625f Merge branch 'master' into left_right_scroll 2019-09-05 21:54:22 +02:00
jerch 97bf796c43 Merge branch 'master' into left_right_scroll 2019-08-23 20:37:13 +02:00
jerch 54ff71f982 Merge branch 'master' into left_right_scroll 2019-08-23 16:45:42 +02:00
Jörg Breitbart f7827c4df8 add basic tests for commands 2019-08-17 18:52:11 +02:00
Jörg Breitbart 4b5709c68d add DECIC/DECDC to interface 2019-08-17 18:13:56 +02:00
Jörg Breitbart 4fe78dfe34 Merge branch 'master' into left_right_scroll 2019-08-17 17:57:31 +02:00
Corentin Surquin a353a8cffe Allow to disable fast scrolling 2019-08-10 19:45:41 +02:00
Corentin Surquin 73a17cfacb Support fast scrolling using a modifier key 2019-08-10 19:45:41 +02:00
Jörg Breitbart 8bc0099434 linter nonsense 2019-08-04 15:37:47 +02:00
Jörg Breitbart 9a903ca1c5 add DECIC/DECDC 2019-08-04 15:32:32 +02:00
Jörg Breitbart 5483a052cc always reset wrapped flag 2019-08-04 15:15:59 +02:00
Jörg Breitbart bd132d1f74 implement left/right scroll 2019-08-04 15:04:40 +02:00
118 changed files with 5063 additions and 2932 deletions
+9 -2
View File
@@ -8,12 +8,19 @@ RUN apt-get update \
# Verify git and process tools are installed # Verify git and process tools are installed
RUN apt-get install -y git procps RUN apt-get install -y git procps
# Install yarn # Install yarn, puppeteer deps
RUN apt-get install -y curl apt-transport-https lsb-release \ RUN apt-get install -y curl apt-transport-https lsb-release \
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
&& apt-get update \ && apt-get update \
&& apt-get -y install --no-install-recommends yarn && apt-get -y install --no-install-recommends \
yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
# https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352
gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
# Clean up # Clean up
RUN apt-get autoremove -y \ RUN apt-get autoremove -y \
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://stackoverflow.com/questions/tagged/xtermjs
about: Please ask and answer questions here.
-8
View File
@@ -1,8 +0,0 @@
---
name: Question
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs
---
🛑 The issue tracker is not for questions 🛑
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs.
+1
View File
@@ -4,6 +4,7 @@ node_modules/
lib/ lib/
out/ out/
out-test/ out-test/
.nyc_output/
Makefile.gyp Makefile.gyp
*.Makefile *.Makefile
*.target.gyp.mk *.target.gyp.mk
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"typescript.preferences.quoteStyle": "single" "typescript.preferences.quoteStyle": "single",
"typescript.preferences.importModuleSpecifier": "non-relative"
} }
+4
View File
@@ -52,3 +52,7 @@ By contributing code to xterm.js you:
holder has explicitly granted the right to use it like this, holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct through a compatible open source license or through a direct
agreement with you.) agreement with you.)
### Test coverage
One area that always needs attention is improving out unit test coverage, you can view the code coverage report on [Azure Pipelines](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) by clicking the Code Coverage tab.
+4 -2
View File
@@ -10,7 +10,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer. - **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
- **Rich unicode support**: Supports CJK, emojis and IMEs. - **Rich unicode support**: Supports CJK, emojis and IMEs.
- **Self-contained**: Requires zero dependencies to work. - **Self-contained**: Requires zero dependencies to work.
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option. - **Accessible**: Screen reader and minimum contrast ratio support can be turned on
- **And much more**: Links, theming, addons, well documented API, etc. - **And much more**: Links, theming, addons, well documented API, etc.
## What xterm.js is not ## What xterm.js is not
@@ -110,7 +110,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2. - [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE. - [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor. - [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js. - [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R. - [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor. - [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud. - [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
@@ -154,6 +154,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components. - [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
- [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `<ng-terminal></ng-terminal>` into your component. - [**NgTerminal**](https://github.com/qwefgh90/ng-terminal): NgTerminal is a web terminal that leverages xterm.js on Angular 7+. You can easily add it into your application by adding `<ng-terminal></ng-terminal>` into your component.
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet. - [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents) [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
+1 -1
View File
@@ -12,7 +12,7 @@ npm install --save xterm-addon-attach
```ts ```ts
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-attach'; import { AttachAddon } from 'xterm-addon-attach';
const terminal = new Terminal(); const terminal = new Terminal();
const attachAddon = new AttachAddon(webSocket); const attachAddon = new AttachAddon(webSocket);
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-attach", "name": "xterm-addon-attach",
"version": "0.2.0", "version": "0.4.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"peerDependencies": { "peerDependencies": {
"xterm": "^3.14.0" "xterm": "^4.0.0"
} }
} }
@@ -20,8 +20,7 @@ describe('AttachAddon', () => {
this.timeout(20000); this.timeout(20000);
browser = await puppeteer.launch({ browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1, headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80, args: [`--window-size=${width},${height}`, `--no-sandbox`]
args: [`--window-size=${width},${height}`]
}); });
page = (await browser.pages())[0]; page = (await browser.pages())[0];
await page.setViewport({ width, height }); await page.setViewport({ width, height });
@@ -43,7 +42,7 @@ describe('AttachAddon', () => {
const server = new WebSocket.Server({ port }); const server = new WebSocket.Server({ port });
server.on('connection', socket => socket.send('foo')); server.on('connection', socket => socket.send('foo'));
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`); await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
server.close(); server.close();
}); });
@@ -54,8 +53,8 @@ describe('AttachAddon', () => {
const server = new WebSocket.Server({ port }); const server = new WebSocket.Server({ port });
const data = new Uint8Array([102, 111, 111]); const data = new Uint8Array([102, 111, 111]);
server.on('connection', socket => socket.send(data)); server.on('connection', socket => socket.send(data));
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}'), { inputUtf8: true }))`); await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
server.close(); server.close();
}); });
}); });
@@ -69,3 +68,12 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.waitForSelector('.xterm-text-layer'); await page.waitForSelector('.xterm-text-layer');
} }
} }
async function pollFor(page: puppeteer.Page, fn: string, val: any): Promise<void> {
const result = await page.evaluate(fn);
if (result !== val) {
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, fn, val)), 10);
});
}
}
+19 -11
View File
@@ -9,13 +9,11 @@ import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
interface IAttachOptions { interface IAttachOptions {
bidirectional?: boolean; bidirectional?: boolean;
inputUtf8?: boolean;
} }
export class AttachAddon implements ITerminalAddon { export class AttachAddon implements ITerminalAddon {
private _socket: WebSocket; private _socket: WebSocket;
private _bidirectional: boolean; private _bidirectional: boolean;
private _utf8: boolean;
private _disposables: IDisposable[] = []; private _disposables: IDisposable[] = [];
constructor(socket: WebSocket, options?: IAttachOptions) { constructor(socket: WebSocket, options?: IAttachOptions) {
@@ -23,20 +21,19 @@ export class AttachAddon implements ITerminalAddon {
// always set binary type to arraybuffer, we do not handle blobs // always set binary type to arraybuffer, we do not handle blobs
this._socket.binaryType = 'arraybuffer'; this._socket.binaryType = 'arraybuffer';
this._bidirectional = (options && options.bidirectional === false) ? false : true; this._bidirectional = (options && options.bidirectional === false) ? false : true;
this._utf8 = !!(options && options.inputUtf8);
} }
public activate(terminal: Terminal): void { public activate(terminal: Terminal): void {
if (this._utf8) { this._disposables.push(
this._disposables.push(addSocketListener(this._socket, 'message', addSocketListener(this._socket, 'message', ev => {
(ev: MessageEvent | Event | CloseEvent) => terminal.writeUtf8(new Uint8Array((ev as any).data as ArrayBuffer)))); const data: ArrayBuffer | string = ev.data;
} else { terminal.write(typeof data === 'string' ? data : new Uint8Array(data));
this._disposables.push(addSocketListener(this._socket, 'message', })
(ev: MessageEvent | Event | CloseEvent) => terminal.write((ev as any).data as string))); );
}
if (this._bidirectional) { if (this._bidirectional) {
this._disposables.push(terminal.onData(data => this._sendData(data))); this._disposables.push(terminal.onData(data => this._sendData(data)));
this._disposables.push(terminal.onBinary(data => this._sendBinary(data)));
} }
this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose())); this._disposables.push(addSocketListener(this._socket, 'close', () => this.dispose()));
@@ -55,9 +52,20 @@ export class AttachAddon implements ITerminalAddon {
} }
this._socket.send(data); this._socket.send(data);
} }
private _sendBinary(data: string): void {
if (this._socket.readyState !== 1) {
return;
}
const buffer = new Uint8Array(data.length);
for (let i = 0; i < data.length; ++i) {
buffer[i] = data.charCodeAt(i) & 255;
}
this._socket.send(buffer);
}
} }
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: MessageEvent | Event | CloseEvent) => any): IDisposable { function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
socket.addEventListener(type, handler); socket.addEventListener(type, handler);
return { return {
dispose: () => { dispose: () => {
@@ -11,14 +11,6 @@ declare module 'xterm-addon-attach' {
* Whether input should be written to the backend. Defaults to `true`. * Whether input should be written to the backend. Defaults to `true`.
*/ */
bidirectional?: boolean; bidirectional?: boolean;
/**
* Whether to use UTF8 binary transport for incoming messages. Defaults to `false`.
* Note: This must be in line with the server side of the websocket.
* Always send string messages from the backend if this options is false,
* otherwise always binary UTF8 data.
*/
inputUtf8?: boolean;
} }
export class AttachAddon implements ITerminalAddon { export class AttachAddon implements ITerminalAddon {
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-fit", "name": "xterm-addon-fit",
"version": "0.2.0", "version": "0.3.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"peerDependencies": { "peerDependencies": {
"xterm": "^3.14.0" "xterm": "^4.0.0"
} }
} }
+118
View File
@@ -0,0 +1,118 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from 'xterm';
const APP = 'http://127.0.0.1:3000/test';
let browser: puppeteer.Browser;
let page: puppeteer.Page;
const width = 1024;
const height = 768;
describe('FitAddon', () => {
before(async function(): Promise<any> {
this.timeout(20000);
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal();
});
after(async () => {
await browser.close();
});
it('no terminal', async function(): Promise<any> {
await page.evaluate(`window.fit = new FitAddon();`);
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
});
describe('proposeDimensions', () => {
afterEach(async () => {
return unloadFit();
});
it('default', async function(): Promise<any> {
await loadFit();
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
cols: 87,
rows: 26
});
});
it('width', async function(): Promise<any> {
await loadFit(1008);
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
cols: 110,
rows: 26
});
});
it('small', async function(): Promise<any> {
await loadFit(1, 1);
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
cols: 2,
rows: 1
});
});
});
describe('fit', () => {
afterEach(async () => {
return unloadFit();
});
it('default', async function(): Promise<any> {
await loadFit();
await page.evaluate(`window.fit.fit()`);
assert.equal(await page.evaluate(`window.term.cols`), 87);
assert.equal(await page.evaluate(`window.term.rows`), 26);
});
it('width', async function(): Promise<any> {
await loadFit(1008);
await page.evaluate(`window.fit.fit()`);
assert.equal(await page.evaluate(`window.term.cols`), 110);
assert.equal(await page.evaluate(`window.term.rows`), 26);
});
it('small', async function(): Promise<any> {
await loadFit(1, 1);
await page.evaluate(`window.fit.fit()`);
assert.equal(await page.evaluate(`window.term.cols`), 2);
assert.equal(await page.evaluate(`window.term.rows`), 1);
});
});
});
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
await page.evaluate(`
window.fit = new FitAddon();
window.term.loadAddon(window.fit);
document.querySelector('#terminal-container').style.width='${width}px';
document.querySelector('#terminal-container').style.height='${height}px';
`);
}
async function unloadFit(): Promise<void> {
await page.evaluate(`window.fit.dispose();`);
}
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
if (options.rendererType === 'dom') {
await page.waitForSelector('.xterm-rows');
} else {
await page.waitForSelector('.xterm-text-layer');
}
}
+6 -3
View File
@@ -17,6 +17,9 @@ interface ITerminalDimensions {
cols: number; cols: number;
} }
const MINIMUM_COLS = 2;
const MINIMUM_ROWS = 1;
export class FitAddon implements ITerminalAddon { export class FitAddon implements ITerminalAddon {
private _terminal: Terminal | undefined; private _terminal: Terminal | undefined;
@@ -49,7 +52,7 @@ export class FitAddon implements ITerminalAddon {
return undefined; return undefined;
} }
if (!this._terminal.element.parentElement) { if (!this._terminal.element || !this._terminal.element.parentElement) {
return undefined; return undefined;
} }
@@ -71,8 +74,8 @@ export class FitAddon implements ITerminalAddon {
const availableHeight = parentElementHeight - elementPaddingVer; const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth; const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
const geometry = { const geometry = {
cols: Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth), cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
rows: Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight) rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
}; };
return geometry; return geometry;
} }
@@ -0,0 +1,70 @@
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
make[3]: Nothing to be done for `libsrc/wrap_curl/CMakeFiles/wrap_curl.dir/build'.
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
[ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
[ 0%] [ 0%] /usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
Built target config_utils
[ 0%] make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
Built target omega
make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
cd /home/yan/ssd/searchtrunk/tmp/release && /usr/bin/cmake -E cmake_depends "Unix Makefiles" /home/yan/ssd/searchtrunk /home/yan/ssd/searchtrunk/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp /home/yan/ssd/searchtrunk/tmp/release/projects/protogen/cpp/CMakeFiles/protogen.dir/DependInfo.cmake --color=
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
Built target morpheus
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
make -f libsrc/base/CMakeFiles/base.dir/build.make libsrc/base/CMakeFiles/base.dir/build
make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
make[3]: Nothing to be done for `libsrc/coroutine/CMakeFiles/coroutine.dir/build'.
Built target wrap_curl
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
[ 0%] [ 0%] [ 0%] [ 0%] Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o
cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/crop_context.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp
Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o
Building CXX object libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o
cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/detector.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp
Built target coroutine
cd /home/yan/ssd/searchtrunk/tmp/release/libsrc/image_crop && /opt/rh/devtoolset-7/root/usr/bin/c++ -DGOGO_USE_LIBEVENT2=1 -DGOGO_USE_OPENSSL_SNI=1 -DHAVE_BOOST -DHAVE_GLIBC -DHAVE_MALLOC_TRIM -DHAVE_PREAD -DHAVE_READAHEAD -D_FILE_OFFSET_BITS=64 -D_STAT_LEMM -pipe -Wall -Wextra -Werror=multichar -Wno-deprecated -Wno-unused-parameter -pthread -fPIC -Woverloaded-virtual -Wnon-virtual-dtor -Werror -std=gnu++11 -Wno-unknown-pragmas -DBOOST_FILESYSTEM_VERSION=3 -DBOOST_NO_CXX11_HDR_CODECVT -DMAGICKCORE_QUANTUM_DEPTH=8 -DMAGICKCORE_HDRI_ENABLE=0 -Wno-unused-local-typedefs -D_GLIBCXX_USE_CXX11_ABI=0 -std=gnu++14 -DCXXHASH128_EXIST -static-libstdc++ -Wno-implicit-fallthrough -Wno-stringop-overflow -O3 -DNDEBUG -DBOOST_UBLAS_NDEBUG -g0 -march=core2 -mssse3 -msse4.1 -I/home/yan/ssd/searchtrunk/tmp/release/__inc -I/home/yan/ssd/searchtrunk/libsrc/image_crop/src -I/home/yan/ssd/searchtrunk/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/boost_1_69_0/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/icu-49.1.2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/protobuf-3.4.0/include -I/home/yan/ssd/searchtrunk/tmp/release -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/opencv-3.0.0m2/include -isystem /home/yan/ssd/searchtrunk/tmp/release/contrib/dlib-19.7p1/include -Wno-array-bounds -o CMakeFiles/image_crop.dir/src/image_crop.cpp.o -c /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp
make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
make[3]: Nothing to be done for `libsrc/base/CMakeFiles/base.dir/build'.
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
make -f projects/protogen/cpp/CMakeFiles/protogen.dir/build.make projects/protogen/cpp/CMakeFiles/protogen.dir/build
[ 0%] Built target base
In file included from /home/yan/ssd/searchtrunk/include/image_crop/detector.hpp:3:0,
from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/detector.cpp:2:
/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
#include <opencv2/dnn.hpp>
^~~~~~~~~~~~~~~~~
compilation terminated.
make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/detector.cpp.o] Error 1
make[3]: *** Waiting for unfinished jobs....
In file included from /home/yan/ssd/searchtrunk/include/image_crop/image_crop.hpp:3:0,
from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/image_crop.cpp:1:
/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
#include <opencv2/dnn.hpp>
^~~~~~~~~~~~~~~~~
compilation terminated.
make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/image_crop.cpp.o] Error 1
In file included from /home/yan/ssd/searchtrunk/libsrc/image_crop/src/crop_context.cpp:1:0:
/home/yan/ssd/searchtrunk/include/image_crop/crop_context.hpp:6:10: fatal error: opencv2/dnn.hpp: No such file or directory
#include <opencv2/dnn.hpp>
^~~~~~~~~~~~~~~~~
compilation terminated.
make[3]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/src/crop_context.cpp.o] Error 1
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
make[2]: *** [libsrc/image_crop/CMakeFiles/image_crop.dir/all] Error 2
make[2]: *** Waiting for unfinished jobs....
make[3]: Entering directory `/home/yan/ssd/searchtrunk/tmp/release'
make[3]: Nothing to be done for `projects/protogen/cpp/CMakeFiles/protogen.dir/build'.
make[3]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
/usr/bin/cmake -E cmake_progress_report /home/yan/ssd/searchtrunk/tmp/release/CMakeFiles 63 64 65 66
[ 25%] Built target protogen
make[2]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release'
make[1]: *** [all] Error 2
make[1]: Leaving directory `/home/yan/ssd/searchtrunk/tmp/release/projects/projectX'
make: *** [install] Error 2
yan@yPC:~/ssd/searchtrunk/projects/projectX$
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-search", "name": "xterm-addon-search",
"version": "0.2.0", "version": "0.4.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"peerDependencies": { "peerDependencies": {
"xterm": "^3.14.0" "xterm": "^4.0.0"
} }
} }
@@ -6,6 +6,8 @@
import * as puppeteer from 'puppeteer'; import * as puppeteer from 'puppeteer';
import { assert } from 'chai'; import { assert } from 'chai';
import { ITerminalOptions } from 'xterm'; import { ITerminalOptions } from 'xterm';
import { readFile } from 'fs';
import { resolve } from 'path';
const APP = 'http://127.0.0.1:3000/test'; const APP = 'http://127.0.0.1:3000/test';
@@ -15,13 +17,12 @@ const width = 800;
const height = 600; const height = 600;
describe('Search Tests', function (): void { describe('Search Tests', function (): void {
this.timeout(200000); this.timeout(20000);
before(async function (): Promise<any> { before(async function (): Promise<any> {
browser = await puppeteer.launch({ browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1, headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80, args: [`--window-size=${width},${height}`, `--no-sandbox`]
args: [`--window-size=${width},${height}`]
}); });
page = (await browser.pages())[0]; page = (await browser.pages())[0];
await page.setViewport({ width, height }); await page.setViewport({ width, height });
@@ -91,13 +92,101 @@ describe('Search Tests', function (): void {
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc'); assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
}); });
it ('Simple Regex', async () => { it('Simple Regex', async () => {
await writeSync('abc123defABCD'); await writeSync('abc123defABCD');
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`); await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc'); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`); await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD'); assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
}); });
it('Search for single result twice should not unselect it', async () => {
await writeSync('abc def');
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
});
describe('Regression tests', () => {
describe('#2444 wrapped line content not being found', () => {
let fixture: string;
before(async () => {
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
fixture = rawFixture.toString()
.replace(/\n/g, '\\n\\r')
.replace(/'/g, '\\\'');
});
it('should find all occurrences using findNext', async () => {
await writeSync(fixture);
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
});
it('should find all occurrences using findPrevious', async () => {
await writeSync(fixture);
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
// Wrap around to first result
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
});
});
});
}); });
async function openTerminal(options: ITerminalOptions = {}): Promise<void> { async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
@@ -111,12 +200,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
} }
async function writeSync(data: string): Promise<void> { async function writeSync(data: string): Promise<void> {
await page.evaluate(`window.term.write('${data}');`); return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
while (true) {
if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
break;
}
}
} }
function makeData(length: number): string { function makeData(length: number): string {
+56 -20
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { Terminal, IDisposable, ITerminalAddon } from 'xterm'; import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
export interface ISearchOptions { export interface ISearchOptions {
regex?: boolean; regex?: boolean;
@@ -12,6 +12,11 @@ export interface ISearchOptions {
incremental?: boolean; incremental?: boolean;
} }
export interface ISearchPosition {
startCol: number;
startRow: number;
}
export interface ISearchResult { export interface ISearchResult {
term: string; term: string;
col: number; col: number;
@@ -59,29 +64,35 @@ export class SearchAddon implements ITerminalAddon {
let startCol = 0; let startCol = 0;
let startRow = 0; let startRow = 0;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) { if (this._terminal.hasSelection()) {
const incremental = searchOptions ? searchOptions.incremental : false; const incremental = searchOptions ? searchOptions.incremental : false;
// Start from the selection end if there is a selection // Start from the selection end if there is a selection
// For incremental search, use existing row // For incremental search, use existing row
const currentSelection = this._terminal.getSelectionPosition()!; currentSelection = this._terminal.getSelectionPosition()!;
startRow = incremental ? currentSelection.startRow : currentSelection.endRow; startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
} }
this._initLinesCache(); this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
// Search startRow // Search startRow
let result = this._findInLine(term, startRow, startCol, searchOptions); let result = this._findInLine(term, searchPosition, searchOptions);
// Search from startRow + 1 to end // Search from startRow + 1 to end
if (!result) { if (!result) {
for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) { for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
searchPosition.startRow = y;
searchPosition.startCol = 0;
// If the current line is wrapped line, increase index of column to ignore the previous scan // If the current line is wrapped line, increase index of column to ignore the previous scan
// Otherwise, reset beginning column index to zero with set new unwrapped line index // Otherwise, reset beginning column index to zero with set new unwrapped line index
result = this._findInLine(term, y, 0, searchOptions); result = this._findInLine(term, searchPosition, searchOptions);
if (result) { if (result) {
break; break;
} }
@@ -90,13 +101,18 @@ export class SearchAddon implements ITerminalAddon {
// If we hit the bottom and didn't search from the very top wrap back up // If we hit the bottom and didn't search from the very top wrap back up
if (!result && startRow !== 0) { if (!result && startRow !== 0) {
for (let y = 0; y < startRow; y++) { for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, 0, searchOptions); searchPosition.startRow = y;
searchPosition.startCol = 0;
result = this._findInLine(term, searchPosition, searchOptions);
if (result) { if (result) {
break; break;
} }
} }
} }
// If there is only one result, return true.
if (!result && currentSelection) return true;
// Set selection and scroll if a result was found // Set selection and scroll if a result was found
return this._selectResult(result); return this._selectResult(result);
} }
@@ -121,31 +137,37 @@ export class SearchAddon implements ITerminalAddon {
const isReverseSearch = true; const isReverseSearch = true;
let startRow = this._terminal.buffer.baseY + this._terminal.rows; let startRow = this._terminal.buffer.baseY + this._terminal.rows;
let startCol = this._terminal.cols; let startCol = this._terminal.cols;
let result: ISearchResult | undefined = undefined; let result: ISearchResult | undefined;
const incremental = searchOptions ? searchOptions.incremental : false; const incremental = searchOptions ? searchOptions.incremental : false;
let currentSelection: ISelectionPosition | undefined;
if (this._terminal.hasSelection()) { if (this._terminal.hasSelection()) {
const currentSelection = this._terminal.getSelectionPosition()!; currentSelection = this._terminal.getSelectionPosition()!;
// Start from selection start if there is a selection // Start from selection start if there is a selection
startRow = currentSelection.startRow; startRow = currentSelection.startRow;
startCol = currentSelection.startColumn; startCol = currentSelection.startColumn;
} }
this._initLinesCache(); this._initLinesCache();
const searchPosition: ISearchPosition = {
startRow,
startCol
};
if (incremental) { if (incremental) {
result = this._findInLine(term, startRow, startCol, searchOptions, false); result = this._findInLine(term, searchPosition, searchOptions, false);
if (!(result && result.row === startRow && result.col === startCol)) { if (!(result && result.row === startRow && result.col === startCol)) {
result = this._findInLine(term, startRow, startCol, searchOptions, true); result = this._findInLine(term, searchPosition, searchOptions, true);
} }
} else { } else {
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
} }
// Search from startRow - 1 to top // Search from startRow - 1 to top
if (!result) { if (!result) {
startCol = this._terminal.cols; searchPosition.startCol = Math.max(searchPosition.startCol, this._terminal.cols);
for (let y = startRow - 1; y >= 0; y--) { for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) { if (result) {
break; break;
} }
@@ -154,13 +176,17 @@ export class SearchAddon implements ITerminalAddon {
// If we hit the top and didn't search from the very bottom wrap back down // If we hit the top and didn't search from the very bottom wrap back down
if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) { if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) {
for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) { for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) {
result = this._findInLine(term, y, startCol, searchOptions, isReverseSearch); searchPosition.startRow = y;
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
if (result) { if (result) {
break; break;
} }
} }
} }
// If there is only one result, return true.
if (!result && currentSelection) return true;
// Set selection and scroll if a result was found // Set selection and scroll if a result was found
return this._selectResult(result); return this._selectResult(result);
} }
@@ -213,18 +239,28 @@ export class SearchAddon implements ITerminalAddon {
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
* text starts on is searched. * text starts on is searched.
* @param term The search term. * @param term The search term.
* @param row The line to start the search from. * @param position The position to start the search.
* @param col The column to start the search from.
* @param searchOptions Search options. * @param searchOptions Search options.
* @return The search result if it was found. * @return The search result if it was found.
*/ */
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined { protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
const terminal = this._terminal!; const terminal = this._terminal!;
let row = searchPosition.startRow;
const col = searchPosition.startCol;
// Ignore wrapped lines, only consider on unwrapped line (first row of command string). // Ignore wrapped lines, only consider on unwrapped line (first row of command string).
const firstLine = terminal.buffer.getLine(row); const firstLine = terminal.buffer.getLine(row);
if (firstLine && firstLine.isWrapped) { if (firstLine && firstLine.isWrapped) {
return; if (isReverseSearch) {
searchPosition.startCol += terminal.cols;
return;
}
// This will iterate until we find the line start.
// When we find it, we will search using the calculated start column.
searchPosition.startRow--;
searchPosition.startCol += terminal.cols;
return this._findInLine(term, searchPosition, searchOptions);
} }
let stringLine = this._linesCache ? this._linesCache[row] : void 0; let stringLine = this._linesCache ? this._linesCache[row] : void 0;
if (stringLine === void 0) { if (stringLine === void 0) {
@@ -344,7 +380,7 @@ export class SearchAddon implements ITerminalAddon {
} }
terminal.select(result.col, result.row, result.term.length); terminal.select(result.col, result.row, result.term.length);
// If it is not in the viewport then we scroll else it just gets selected // If it is not in the viewport then we scroll else it just gets selected
if (result.row > (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) { if (result.row >= (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) {
let scroll = result.row - terminal.buffer.viewportY; let scroll = result.row - terminal.buffer.viewportY;
scroll = scroll - Math.floor(terminal.rows / 2); scroll = scroll - Math.floor(terminal.rows / 2);
terminal.scrollLines(scroll); terminal.scrollLines(scroll);
+2 -2
View File
@@ -17,7 +17,7 @@ declare module 'xterm-addon-search' {
/** /**
* Whether to search for a whole word, the result is only valid if it's * Whether to search for a whole word, the result is only valid if it's
* suppounded in "non-word" characters such as `_`, `(`, `)` or space. * surrounded in "non-word" characters such as `_`, `(`, `)` or space.
*/ */
wholeWord?: boolean; wholeWord?: boolean;
@@ -27,7 +27,7 @@ declare module 'xterm-addon-search' {
caseSensitive?: boolean; caseSensitive?: boolean;
/** /**
* Whether to do an indcremental search, this will expand the selection if it * Whether to do an incremental search, this will expand the selection if it
* still matches the term the user typed. Note that this only affects * still matches the term the user typed. Note that this only affects
* `findNext`, not `findPrevious`. * `findNext`, not `findPrevious`.
*/ */
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-web-links", "name": "xterm-addon-web-links",
"version": "0.2.0", "version": "0.2.1",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"peerDependencies": { "peerDependencies": {
"xterm": "^3.14.0" "xterm": "^4.0.0"
} }
} }
@@ -19,8 +19,7 @@ describe('WebLinksAddon', () => {
this.timeout(10000); this.timeout(10000);
browser = await puppeteer.launch({ browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1, headless: process.argv.indexOf('--headless') !== -1,
slowMo: 80, args: [`--window-size=${width},${height}`, `--no-sandbox`]
args: [`--window-size=${width},${height}`]
}); });
page = (await browser.pages())[0]; page = (await browser.pages())[0];
await page.setViewport({ width, height }); await page.setViewport({ width, height });
@@ -54,22 +53,21 @@ describe('WebLinksAddon', () => {
async function testHostName(hostname: string): Promise<void> { async function testHostName(hostname: string): Promise<void> {
await openTerminal({ rendererType: 'dom' }); await openTerminal({ rendererType: 'dom' });
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
await page.evaluate(` const data = ` http://${hostname} \\r\\n` +
window.term.writeln(' http://${hostname} '); ` http://${hostname}/a~b#c~d?e~f \\r\\n` +
window.term.writeln(' http://${hostname}/a~b#c~d?e~f '); ` http://${hostname}/colon:test \\r\\n` +
window.term.writeln(' http://${hostname}/colon:test '); ` http://${hostname}/colon:test: \\r\\n` +
window.term.writeln(' http://${hostname}/colon:test: '); `"http://${hostname}/"\\r\\n` +
window.term.writeln('"http://${hostname}/"'); `\\'http://${hostname}/\\'\\r\\n` +
window.term.writeln('\\'http://${hostname}/\\''); `http://${hostname}/subpath/+/id`;
window.term.writeln('http://${hostname}/subpath/+/id'); await writeSync(page, data);
`); await pollForLinkAtCell(3, 1, `http://${hostname}`);
assert.equal(await getLinkAtCell(3, 1), `http://${hostname}`); await pollForLinkAtCell(3, 2, `http://${hostname}/a~b#c~d?e~f`);
assert.equal(await getLinkAtCell(3, 2), `http://${hostname}/a~b#c~d?e~f`); await pollForLinkAtCell(3, 3, `http://${hostname}/colon:test`);
assert.equal(await getLinkAtCell(3, 3), `http://${hostname}/colon:test`); await pollForLinkAtCell(3, 4, `http://${hostname}/colon:test`);
assert.equal(await getLinkAtCell(3, 4), `http://${hostname}/colon:test`); await pollForLinkAtCell(2, 5, `http://${hostname}/`);
assert.equal(await getLinkAtCell(2, 5), `http://${hostname}/`); await pollForLinkAtCell(2, 6, `http://${hostname}/`);
assert.equal(await getLinkAtCell(2, 6), `http://${hostname}/`); await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
assert.equal(await getLinkAtCell(1, 7), `http://${hostname}/subpath/+/id`);
} }
async function openTerminal(options: ITerminalOptions = {}): Promise<void> { async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
@@ -82,8 +80,30 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
} }
} }
async function getLinkAtCell(col: number, row: number): Promise<string> { async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
const rowSelector = `.xterm-rows > :nth-child(${row})`; const rowSelector = `.xterm-rows > :nth-child(${row})`;
await page.hover(`${rowSelector} > :nth-child(${col})`); // Ensure the hover element exists before trying to hover it
return await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`); await pollFor(page, `!!document.querySelector('${rowSelector} > :nth-child(${col})')`, true);
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
}
async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise<void>): Promise<void> {
if (preFn) {
await preFn();
}
const result = await page.evaluate(fn);
if (result !== val) {
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, fn, val, preFn)), 10);
});
}
}
async function writeSync(page: puppeteer.Page, data: string): Promise<void> {
await page.evaluate(`
window.ready = false;
window.term.write('${data}', () => window.ready = true);
`);
await pollFor(page, 'window.ready', true);
} }
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "xterm-addon-webgl", "name": "xterm-addon-webgl",
"version": "0.2.0", "version": "0.4.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
@@ -16,6 +16,6 @@
"prepublishOnly": "npm run package" "prepublishOnly": "npm run package"
}, },
"peerDependencies": { "peerDependencies": {
"xterm": "^3.14.0" "xterm": "^4.0.0"
} }
} }
@@ -1,28 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { CellData } from 'common/buffer/CellData';
import { FLAGS } from './Constants';
import { IBufferLine } from 'common/Types';
export function getCompatAttr(bufferLine: IBufferLine, index: number): number {
// TODO: Need to move WebGL over to the new system and remove this block
const cell = new CellData();
bufferLine.loadCell(index, cell);
const oldBg = cell.getBgColor() === -1 ? 256 : cell.getBgColor();
const oldFg = cell.getFgColor() === -1 ? 256 : cell.getFgColor();
const oldAttr =
(cell.isBold() ? FLAGS.BOLD : 0) |
(cell.isUnderline() ? FLAGS.UNDERLINE : 0) |
(cell.isBlink() ? FLAGS.BLINK : 0) |
(cell.isInverse() ? FLAGS.INVERSE : 0) |
(cell.isDim() ? FLAGS.DIM : 0) |
(cell.isItalic() ? FLAGS.ITALIC : 0);
const attrCompat =
oldBg |
(oldFg << 9) |
(oldAttr << 18);
return attrCompat;
}
@@ -1,14 +0,0 @@
/**
* @license MIT
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
*/
import { IColor } from 'browser/Types';
export function getLuminance(color: IColor): number {
// Coefficients taken from: https://www.w3.org/TR/AERT/#color-contrast
const r = color.rgba >> 24 & 0xff;
const g = color.rgba >> 16 & 0xff;
const b = color.rgba >> 8 & 0xff;
return (0.299 * r + 0.587 * g + 0.114 * b) / 255;
}
-15
View File
@@ -1,15 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
// TODO: Should be removed after chardata workaround is fixed
export const enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16,
DIM = 32,
ITALIC = 64
}
+17 -30
View File
@@ -6,12 +6,10 @@
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types'; import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { INDICIES_PER_CELL } from './WebglRenderer'; import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel';
import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { fill } from 'common/TypedArrayUtils'; import { fill } from 'common/TypedArrayUtils';
import { slice } from './TypedArray'; import { slice } from './TypedArray';
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { getLuminance } from './ColorUtils';
import { Terminal, IBufferLine } from 'xterm'; import { Terminal, IBufferLine } from 'xterm';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types'; import { IRenderDimensions } from 'browser/renderer/Types';
@@ -103,11 +101,7 @@ export class GlyphRenderer {
private _dimensions: IRenderDimensions private _dimensions: IRenderDimensions
) { ) {
const gl = this._gl; const gl = this._gl;
const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource)); const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
if (program === undefined) {
throw new Error('Could not create WebGL program');
}
this._program = program; this._program = program;
// Uniform locations // Uniform locations
@@ -172,11 +166,11 @@ export class GlyphRenderer {
return this._atlas ? this._atlas.beginFrame() : true; return this._atlas ? this._atlas.beginFrame() : true;
} }
public updateCell(x: number, y: number, code: number, attr: number, bg: number, fg: number, chars: string): void { public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void {
this._updateCell(this._vertices.attributes, x, y, code, attr, bg, fg, chars); this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars);
} }
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, attr: number, bg: number, fg: number, chars?: string): void { private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void {
const terminal = this._terminal; const terminal = this._terminal;
const i = (y * terminal.cols + x) * INDICES_PER_CELL; const i = (y * terminal.cols + x) * INDICES_PER_CELL;
@@ -189,12 +183,12 @@ export class GlyphRenderer {
let rasterizedGlyph: IRasterizedGlyph; let rasterizedGlyph: IRasterizedGlyph;
if (!this._atlas) { if (!this._atlas) {
throw new Error('atlas must be set before updating cell'); return;
} }
if (chars && chars.length > 1) { if (chars && chars.length > 1) {
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, attr, bg, fg); rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
} else { } else {
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, attr, bg, fg); rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
} }
// Fill empty if no glyph was found // Fill empty if no glyph was found
@@ -223,58 +217,51 @@ export class GlyphRenderer {
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0); this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
// TODO: Make fg and bg configurable, currently since the buffer doesn't const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
// support truecolor the char atlas cannot store it.
const lumi = getLuminance(this._colors.background);
const fg = lumi > 0.5 ? 7 : 0;
const bg = lumi > 0.5 ? 0 : 7;
if (columnSelectMode) { if (columnSelectMode) {
const startCol = model.selection.startCol; const startCol = model.selection.startCol;
const width = model.selection.endCol - startCol; const width = model.selection.endCol - startCol;
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1; const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) { for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
this._updateSelectionRange(startCol, startCol + width, y, model, bg, fg); this._updateSelectionRange(startCol, startCol + width, y, model, bg);
} }
} else { } else {
// Draw first row // Draw first row
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0; const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg, fg); this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);
// Draw middle rows // Draw middle rows
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0); const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) { for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
this._updateSelectionRange(0, startRowEndCol, y, model, bg, fg); this._updateSelectionRange(0, startRowEndCol, y, model, bg);
} }
// Draw final row // Draw final row
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) { if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewportStartRow // Only draw viewportEndRow if it's not the same as viewportStartRow
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols; const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg, fg); this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
} }
} }
} }
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number, fg: number): void { private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
const terminal = this._terminal; const terminal = this._terminal;
const row = y + terminal.buffer.viewportY; const row = y + terminal.buffer.viewportY;
let line: IBufferLine | undefined; let line: IBufferLine | undefined;
for (let x = startCol; x < endCol; x++) { for (let x = startCol; x < endCol; x++) {
const offset = (y * this._terminal.cols + x) * INDICIES_PER_CELL; const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
// Because the cache uses attr as a lookup key it needs to contain the selection colors as well
let attr = model.cells[offset + 1];
attr = attr & ~0x3ffff | bg << 9 | fg;
const code = model.cells[offset]; const code = model.cells[offset];
if (code & COMBINED_CHAR_BIT_MASK) { if (code & COMBINED_CHAR_BIT_MASK) {
if (!line) { if (!line) {
line = terminal.buffer.getLine(row); line = terminal.buffer.getLine(row);
} }
const chars = line!.getCell(x)!.char; const chars = line!.getCell(x)!.char;
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg, chars); this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars);
} else { } else {
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], attr, bg, fg); this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]);
} }
} }
} }
@@ -6,12 +6,11 @@
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils'; import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types'; import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils'; import { fill } from 'common/TypedArrayUtils';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { Attributes, FgFlags } from 'common/buffer/Constants';
import { is256Color } from './atlas/CharAtlasUtils';
import { DEFAULT_COLOR } from 'common/buffer/Constants';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { IColorSet, IColor } from 'browser/Types'; import { IColorSet, IColor } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types'; import { IRenderDimensions } from 'browser/renderer/Types';
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
const enum VertexAttribLocations { const enum VertexAttribLocations {
POSITION = 0, POSITION = 0,
@@ -23,13 +22,13 @@ const enum VertexAttribLocations {
const vertexShaderSource = `#version 300 es const vertexShaderSource = `#version 300 es
layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position; layout (location = ${VertexAttribLocations.POSITION}) in vec2 a_position;
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size; layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
layout (location = ${VertexAttribLocations.COLOR}) in vec3 a_color; layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad; layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
uniform mat4 u_projection; uniform mat4 u_projection;
uniform vec2 u_resolution; uniform vec2 u_resolution;
out vec3 v_color; out vec4 v_color;
void main() { void main() {
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution; vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
@@ -40,12 +39,12 @@ void main() {
const fragmentShaderSource = `#version 300 es const fragmentShaderSource = `#version 300 es
precision lowp float; precision lowp float;
in vec3 v_color; in vec4 v_color;
out vec4 outColor; out vec4 outColor;
void main() { void main() {
outColor = vec4(v_color, 1); outColor = v_color;
}`; }`;
interface IVertices { interface IVertices {
@@ -156,7 +155,7 @@ export class RectangleRenderer {
private _updateCachedColors(): void { private _updateCachedColors(): void {
this._bgFloat = this._colorToFloat32Array(this._colors.background); this._bgFloat = this._colorToFloat32Array(this._colors.background);
this._selectionFloat = this._colorToFloat32Array(this._colors.selection); this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
} }
private _updateViewportRectangle(): void { private _updateViewportRectangle(): void {
@@ -247,47 +246,73 @@ export class RectangleRenderer {
for (let y = 0; y < terminal.rows; y++) { for (let y = 0; y < terminal.rows; y++) {
let currentStartX = -1; let currentStartX = -1;
let currentBg = DEFAULT_COLOR; let currentBg = 0;
let currentFg = 0;
let currentInverse = false;
for (let x = 0; x < terminal.cols; x++) { for (let x = 0; x < terminal.cols; x++) {
const modelIndex = ((y * terminal.cols) + x) * 4; const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
const bg = model.cells[modelIndex + 2]; const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
if (bg !== currentBg) { const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
const inverse = !!(fg & FgFlags.INVERSE);
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
// A rectangle needs to be drawn if going from non-default to another color // A rectangle needs to be drawn if going from non-default to another color
if (currentBg !== DEFAULT_COLOR) { if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE; const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentBg, currentStartX, x, y); this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
} }
currentStartX = x; currentStartX = x;
currentBg = bg; currentBg = bg;
currentFg = fg;
currentInverse = inverse;
} }
} }
// Finish rectangle if it's still going // Finish rectangle if it's still going
if (currentBg !== DEFAULT_COLOR) { if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
const offset = rectangleCount++ * INDICES_PER_RECTANGLE; const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
this._updateRectangle(vertices, offset, currentBg, currentStartX, terminal.cols, y); this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
} }
} }
vertices.count = rectangleCount; vertices.count = rectangleCount;
} }
private _updateRectangle(vertices: IVertices, offset: number, bg: number, startX: number, endX: number, y: number): void { private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
let color: IColor | null = null; let rgba: number | undefined;
if (bg === INVERTED_DEFAULT_COLOR) { if (fg & FgFlags.INVERSE) {
color = this._colors.foreground; switch (fg & Attributes.CM_MASK) {
} else if (is256Color(bg)) { case Attributes.CM_P16:
color = this._colors.ansi[bg]; case Attributes.CM_P256:
rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (fg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.foreground.rgba;
}
} else { } else {
// TODO: Add support for true color switch (bg & Attributes.CM_MASK) {
color = this._colors.foreground; case Attributes.CM_P16:
case Attributes.CM_P256:
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
break;
case Attributes.CM_RGB:
rgba = (bg & Attributes.RGB_MASK) << 8;
break;
case Attributes.CM_DEFAULT:
default:
rgba = this._colors.background.rgba;
}
} }
if (vertices.attributes.length < offset + 4) { if (vertices.attributes.length < offset + 4) {
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE); vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
} }
const x1 = startX * this._dimensions.scaledCellWidth; const x1 = startX * this._dimensions.scaledCellWidth;
const y1 = y * this._dimensions.scaledCellHeight; const y1 = y * this._dimensions.scaledCellHeight;
const r = ((color.rgba >> 24) & 0xFF) / 255; const r = ((rgba >> 24) & 0xFF) / 255;
const g = ((color.rgba >> 16) & 0xFF) / 255; const g = ((rgba >> 16) & 0xFF) / 255;
const b = ((color.rgba >> 8 ) & 0xFF) / 255; const b = ((rgba >> 8 ) & 0xFF) / 255;
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1); this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
} }
+3 -1
View File
@@ -6,7 +6,9 @@
import { IRenderModel, ISelectionRenderModel } from './Types'; import { IRenderModel, ISelectionRenderModel } from './Types';
import { fill } from 'common/TypedArrayUtils'; import { fill } from 'common/TypedArrayUtils';
export const RENDER_MODEL_INDICIES_PER_CELL = 4; export const RENDER_MODEL_INDICIES_PER_CELL = 3;
export const RENDER_MODEL_BG_OFFSET = 1;
export const RENDER_MODEL_FG_OFFSET = 2;
export const COMBINED_CHAR_BIT_MASK = 0x80000000; export const COMBINED_CHAR_BIT_MASK = 0x80000000;
+1 -1
View File
@@ -4,7 +4,7 @@
*/ */
export interface IRasterizedGlyphSet { export interface IRasterizedGlyphSet {
[flags: number]: IRasterizedGlyph; [bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
} }
/** /**
+10 -2
View File
@@ -9,20 +9,28 @@ import { IRenderService } from 'browser/services/Services';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
export class WebglAddon implements ITerminalAddon { export class WebglAddon implements ITerminalAddon {
private _terminal?: Terminal;
constructor( constructor(
private _preserveDrawingBuffer?: boolean private _preserveDrawingBuffer?: boolean
) {} ) {}
public activate(terminal: Terminal): void { public activate(terminal: Terminal): void {
if (!terminal.element) { if (!terminal.element) {
throw new Error('Cannot activate WebglRendererAddon before Terminal.open'); throw new Error('Cannot activate WebglAddon before Terminal.open');
} }
this._terminal = terminal;
const renderService: IRenderService = (<any>terminal)._core._renderService; const renderService: IRenderService = (<any>terminal)._core._renderService;
const colors: IColorSet = (<any>terminal)._core._colorManager.colors; const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer)); renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
} }
public dispose(): void { public dispose(): void {
throw new Error('WebglRendererAddon.dispose Not yet implemented'); if (!this._terminal) {
throw new Error('Cannot dispose WebglAddon because it is activated');
}
const renderService: IRenderService = (<any>this._terminal)._core._renderService;
renderService.setRenderer((<any>this._terminal)._core._createRenderer());
renderService.onResize(this._terminal.cols, this._terminal.rows);
} }
} }
+692 -66
View File
@@ -4,9 +4,10 @@
*/ */
import * as puppeteer from 'puppeteer'; import * as puppeteer from 'puppeteer';
import { assert } from 'chai';
import { ITerminalOptions } from '../../../src/Types'; import { ITerminalOptions } from '../../../src/Types';
import { ITheme } from 'xterm'; import { ITheme } from 'xterm';
import { assert } from 'chai';
import deepEqual = require('deep-equal');
const APP = 'http://127.0.0.1:3000/test'; const APP = 'http://127.0.0.1:3000/test';
@@ -16,31 +17,20 @@ const width = 800;
const height = 600; const height = 600;
describe('WebGL Renderer Integration Tests', function(): void { describe('WebGL Renderer Integration Tests', function(): void {
this.timeout(20000); it('dispose removes renderer canvases', async () => {
await setupBrowser();
before(async function(): Promise<any> { assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
browser = await puppeteer.launch({ await page.evaluate(`addon.dispose()`);
headless: process.argv.indexOf('--headless') !== -1, assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 0);
slowMo: 80, await browser.close();
args: [`--window-size=${width},${height}`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal();
await page.evaluate(`window.term.loadAddon(new WebglAddon(true));`);
}); });
after(() => { describe('colors', () => {
browser.close(); before(async () => setupBrowser());
}); after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
beforeEach(async () => { it('foreground 0-15', async () => {
await page.evaluate(`window.term.reset()`);
});
describe('WebGL Renderer', () => {
it('foreground colors normal', async function(): Promise<any> {
const theme: ITheme = { const theme: ITheme = {
black: '#010203', black: '#010203',
red: '#040506', red: '#040506',
@@ -53,17 +43,17 @@ describe('WebGL Renderer Integration Tests', function(): void {
}; };
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`); await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
}); });
it('foreground colors bright', async function(): Promise<any> { it('foreground 0-7 drawBoldTextInBrightColors', async () => {
const theme: ITheme = { const theme: ITheme = {
brightBlack: '#010203', brightBlack: '#010203',
brightRed: '#040506', brightRed: '#040506',
@@ -74,19 +64,22 @@ describe('WebGL Renderer Integration Tests', function(): void {
brightCyan: '#131415', brightCyan: '#131415',
brightWhite: '#161718' brightWhite: '#161718'
}; };
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await page.evaluate(`
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`); window.term.setOption('theme', ${JSON.stringify(theme)});
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); window.term.setOption('drawBoldTextInBrightColors', true);
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); `);
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
}); });
it('background colors normal', async function(): Promise<any> { it('background 0-15', async () => {
const theme: ITheme = { const theme: ITheme = {
black: '#010203', black: '#010203',
red: '#040506', red: '#040506',
@@ -99,17 +92,86 @@ describe('WebGL Renderer Integration Tests', function(): void {
}; };
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `); await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
}); });
it('background colors bright', async function(): Promise<any> { it('foreground 0-15 inverse', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
green: '#070809',
yellow: '#0a0b0c',
blue: '#0d0e0f',
magenta: '#101112',
cyan: '#131415',
white: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('background 0-15 inverse', async () => {
const theme: ITheme = {
black: '#010203',
red: '#040506',
green: '#070809',
yellow: '#0a0b0c',
blue: '#0d0e0f',
magenta: '#101112',
cyan: '#131415',
white: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground 0-15 bright', async () => {
const theme: ITheme = {
brightBlack: '#010203',
brightRed: '#040506',
brightGreen: '#070809',
brightYellow: '#0a0b0c',
brightBlue: '#0d0e0f',
brightMagenta: '#101112',
brightCyan: '#131415',
brightWhite: '#161718'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('background 0-15 bright', async () => {
const theme: ITheme = { const theme: ITheme = {
brightBlack: '#010203', brightBlack: '#010203',
brightRed: '#040506', brightRed: '#040506',
@@ -122,14 +184,538 @@ describe('WebGL Renderer Integration Tests', function(): void {
}; };
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`); await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `); await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
assert.deepEqual(await getCellColor(1, 1), [1, 2, 3, 255]); await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
assert.deepEqual(await getCellColor(2, 1), [4, 5, 6, 255]); await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
assert.deepEqual(await getCellColor(3, 1), [7, 8, 9, 255]); await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
assert.deepEqual(await getCellColor(4, 1), [10, 11, 12, 255]); await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
assert.deepEqual(await getCellColor(5, 1), [13, 14, 15, 255]); await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
assert.deepEqual(await getCellColor(6, 1), [16, 17, 18, 255]); await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
assert.deepEqual(await getCellColor(7, 1), [19, 20, 21, 255]); await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
assert.deepEqual(await getCellColor(8, 1), [22, 23, 24, 255]); await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
});
it('foreground 16-255', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('background 16-255', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[48;5;${16 + y * 16 + x}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('foreground 16-255 inverse', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[7;38;5;${16 + y * 16 + x}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('background 16-255 inverse', async () => {
let data = '';
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 240 / 16; y++) {
for (let x = 0; x < 16; x++) {
const cssColor = COLORS_16_TO_255[y * 16 + x];
const r = parseInt(cssColor.substr(1, 2), 16);
const g = parseInt(cssColor.substr(3, 2), 16);
const b = parseInt(cssColor.substr(5, 2), 16);
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
}
}
});
it('foreground true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('background true color red', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;${i};0;0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('foreground true color green', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('background true color green', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;0;${i};0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('foreground true color blue', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('background true color blue', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;0;0;${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('foreground true color grey', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
it('background true color grey', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[48;2;${i};${i};${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
it('foreground true color red inverse', async function(): Promise<void> {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\x1b[7;38;2;${i};0;0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('background true color red inverse', async function(): Promise<void> {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;${i};0;0m█\\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, 0, 0, 255]);
}
}
});
it('foreground true color green inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;38;2;0;${i};0m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('background true color green inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, i, 0, 255]);
}
}
});
it('foreground true color blue inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;38;2;0;0;${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('background true color blue inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, i, 255]);
}
}
});
it('foreground true color grey inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;38;2;${i};${i};${i}m \x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
it('background true color grey inverse', async () => {
let data = '';
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`;
}
data += '\\r\\n';
}
await writeSync(data);
for (let y = 0; y < 16; y++) {
for (let x = 0; x < 16; x++) {
const i = y * 16 + x;
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
}
}
});
});
describe('minimumContrastRatio', async () => {
before(async () => setupBrowser());
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
it('should adjust 0-15 colors on black background', async () => {
const theme: ITheme = {
black: '#2e3436',
red: '#cc0000',
green: '#4e9a06',
yellow: '#c4a000',
blue: '#3465a4',
magenta: '#75507b',
cyan: '#06989a',
white: '#d3d7cf',
brightBlack: '#555753',
brightRed: '#ef2929',
brightGreen: '#8ae234',
brightYellow: '#fce94f',
brightBlue: '#729fcf',
brightMagenta: '#ad7fa8',
brightCyan: '#34e2e2',
brightWhite: '#eeeeec'
};
await page.evaluate(`
window.term.setOption('theme', ${JSON.stringify(theme)});
window.term.setOption('minimumContrastRatio', 1);
`);
await writeSync(
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
);
// Validate before minimumContrastRatio is applied
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]);
await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]);
await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]);
await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]);
await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]);
await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]);
await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]);
await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]);
await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]);
await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]);
await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]);
await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]);
await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
// Setting and check for minimum contrast values, note that these are note
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]);
await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]);
await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]);
await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]);
await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]);
await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]);
await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]);
await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]);
await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]);
await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]);
await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]);
await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]);
await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]);
await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]);
// Unchanged
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
});
it('should adjust 0-15 colors on white background', async () => {
const theme: ITheme = {
background: '#ffffff',
black: '#2e3436',
red: '#cc0000',
green: '#4e9a06',
yellow: '#c4a000',
blue: '#3465a4',
magenta: '#75507b',
cyan: '#06989a',
white: '#d3d7cf',
brightBlack: '#555753',
brightRed: '#ef2929',
brightGreen: '#8ae234',
brightYellow: '#fce94f',
brightBlue: '#729fcf',
brightMagenta: '#ad7fa8',
brightCyan: '#34e2e2',
brightWhite: '#eeeeec'
};
await page.evaluate(`
window.term.setOption('theme', ${JSON.stringify(theme)});
window.term.setOption('minimumContrastRatio', 1);
`);
await writeSync(
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
);
// Validate before minimumContrastRatio is applied
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
await pollFor(page, () => getCellColor(2, 1), [0xcc, 0x00, 0x00, 255]);
await pollFor(page, () => getCellColor(3, 1), [0x4e, 0x9a, 0x06, 255]);
await pollFor(page, () => getCellColor(4, 1), [0xc4, 0xa0, 0x00, 255]);
await pollFor(page, () => getCellColor(5, 1), [0x34, 0x65, 0xa4, 255]);
await pollFor(page, () => getCellColor(6, 1), [0x75, 0x50, 0x7b, 255]);
await pollFor(page, () => getCellColor(7, 1), [0x06, 0x98, 0x9a, 255]);
await pollFor(page, () => getCellColor(8, 1), [0xd3, 0xd7, 0xcf, 255]);
await pollFor(page, () => getCellColor(1, 2), [0x55, 0x57, 0x53, 255]);
await pollFor(page, () => getCellColor(2, 2), [0xef, 0x29, 0x29, 255]);
await pollFor(page, () => getCellColor(3, 2), [0x8a, 0xe2, 0x34, 255]);
await pollFor(page, () => getCellColor(4, 2), [0xfc, 0xe9, 0x4f, 255]);
await pollFor(page, () => getCellColor(5, 2), [0x72, 0x9f, 0xcf, 255]);
await pollFor(page, () => getCellColor(6, 2), [0xad, 0x7f, 0xa8, 255]);
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
// Setting and check for minimum contrast values, note that these are note
// exact to the contrast ratio, if the increase luminance algorithm
// changes then these will probably fail
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]);
await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]);
await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]);
await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]);
await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]);
await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]);
await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]);
await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]);
await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]);
await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]);
await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]);
await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]);
await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]);
await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]);
await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]);
await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]);
});
});
describe('allowTransparency', async () => {
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true}));
after(async () => browser.close());
beforeEach(async () => page.evaluate(`window.term.reset()`));
it('transparent background inverse', async () => {
const theme: ITheme = {
background: '#ff000080'
};
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
const data = `\\x1b[7m█\x1b[0m`;
await writeSync(data);
// Inverse background should be opaque
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
}); });
}); });
}); });
@@ -145,12 +731,7 @@ async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
} }
async function writeSync(data: string): Promise<void> { async function writeSync(data: string): Promise<void> {
await page.evaluate(`window.term.write('${data}');`); return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
while (true) {
if (await page.evaluate(`window.term._core.writeBuffer.length === 0`)) {
break;
}
}
} }
async function getCellColor(col: number, row: number): Promise<number[]> { async function getCellColor(col: number, row: number): Promise<number[]> {
@@ -166,3 +747,48 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
`); `);
return await page.evaluate(`Array.from(window.result)`); return await page.evaluate(`Array.from(window.result)`);
} }
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
browser = await puppeteer.launch({
headless: process.argv.indexOf('--headless') !== -1,
args: [`--window-size=${width},${height}`, `--no-sandbox`]
});
page = (await browser.pages())[0];
await page.setViewport({ width, height });
await page.goto(APP);
await openTerminal(options);
await page.evaluate(`
window.addon = new WebglAddon(true);
window.term.loadAddon(window.addon);
`);
}
export async function pollFor<T>(page: puppeteer.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>): Promise<void> {
if (preFn) {
await preFn();
}
const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
if (!deepEqual(result, val)) {
return new Promise<void>(r => {
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1);
});
}
}
const COLORS_16_TO_255 = [
'#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
'#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
'#00ff87', '#00ffaf', '#00ffd7', '#00ffff', '#5f0000', '#5f005f', '#5f0087', '#5f00af', '#5f00d7', '#5f00ff', '#5f5f00', '#5f5f5f', '#5f5f87', '#5f5faf', '#5f5fd7', '#5f5fff',
'#5f8700', '#5f875f', '#5f8787', '#5f87af', '#5f87d7', '#5f87ff', '#5faf00', '#5faf5f', '#5faf87', '#5fafaf', '#5fafd7', '#5fafff', '#5fd700', '#5fd75f', '#5fd787', '#5fd7af',
'#5fd7d7', '#5fd7ff', '#5fff00', '#5fff5f', '#5fff87', '#5fffaf', '#5fffd7', '#5fffff', '#870000', '#87005f', '#870087', '#8700af', '#8700d7', '#8700ff', '#875f00', '#875f5f',
'#875f87', '#875faf', '#875fd7', '#875fff', '#878700', '#87875f', '#878787', '#8787af', '#8787d7', '#8787ff', '#87af00', '#87af5f', '#87af87', '#87afaf', '#87afd7', '#87afff',
'#87d700', '#87d75f', '#87d787', '#87d7af', '#87d7d7', '#87d7ff', '#87ff00', '#87ff5f', '#87ff87', '#87ffaf', '#87ffd7', '#87ffff', '#af0000', '#af005f', '#af0087', '#af00af',
'#af00d7', '#af00ff', '#af5f00', '#af5f5f', '#af5f87', '#af5faf', '#af5fd7', '#af5fff', '#af8700', '#af875f', '#af8787', '#af87af', '#af87d7', '#af87ff', '#afaf00', '#afaf5f',
'#afaf87', '#afafaf', '#afafd7', '#afafff', '#afd700', '#afd75f', '#afd787', '#afd7af', '#afd7d7', '#afd7ff', '#afff00', '#afff5f', '#afff87', '#afffaf', '#afffd7', '#afffff',
'#d70000', '#d7005f', '#d70087', '#d700af', '#d700d7', '#d700ff', '#d75f00', '#d75f5f', '#d75f87', '#d75faf', '#d75fd7', '#d75fff', '#d78700', '#d7875f', '#d78787', '#d787af',
'#d787d7', '#d787ff', '#d7af00', '#d7af5f', '#d7af87', '#d7afaf', '#d7afd7', '#d7afff', '#d7d700', '#d7d75f', '#d7d787', '#d7d7af', '#d7d7d7', '#d7d7ff', '#d7ff00', '#d7ff5f',
'#d7ff87', '#d7ffaf', '#d7ffd7', '#d7ffff', '#ff0000', '#ff005f', '#ff0087', '#ff00af', '#ff00d7', '#ff00ff', '#ff5f00', '#ff5f5f', '#ff5f87', '#ff5faf', '#ff5fd7', '#ff5fff',
'#ff8700', '#ff875f', '#ff8787', '#ff87af', '#ff87d7', '#ff87ff', '#ffaf00', '#ffaf5f', '#ffaf87', '#ffafaf', '#ffafd7', '#ffafff', '#ffd700', '#ffd75f', '#ffd787', '#ffd7af',
'#ffd7d7', '#ffd7ff', '#ffff00', '#ffff5f', '#ffff87', '#ffffaf', '#ffffd7', '#ffffff', '#080808', '#121212', '#1c1c1c', '#262626', '#303030', '#3a3a3a', '#444444', '#4e4e4e',
'#585858', '#626262', '#6c6c6c', '#767676', '#808080', '#8a8a8a', '#949494', '#9e9e9e', '#a8a8a8', '#b2b2b2', '#bcbcbc', '#c6c6c6', '#d0d0d0', '#dadada', '#e4e4e4', '#eeeeee'
];
+40 -58
View File
@@ -11,19 +11,15 @@ import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { WebglCharAtlas } from './atlas/WebglCharAtlas'; import { WebglCharAtlas } from './atlas/WebglCharAtlas';
import { RectangleRenderer } from './RectangleRenderer'; import { RectangleRenderer } from './RectangleRenderer';
import { IWebGL2RenderingContext } from './Types'; import { IWebGL2RenderingContext } from './Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/Lifecycle';
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from 'common/buffer/Constants'; import { NULL_CELL_CODE } from 'common/buffer/Constants';
import { Terminal } from 'xterm'; import { Terminal, IEvent } from 'xterm';
import { getLuminance } from './ColorUtils';
import { IRenderLayer } from './renderLayer/Types'; import { IRenderLayer } from './renderLayer/Types';
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types'; import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { FLAGS } from './Constants'; import { EventEmitter } from 'common/EventEmitter';
import { getCompatAttr } from './CharDataCompat'; import { CellData } from 'common/buffer/CellData';
export const INDICIES_PER_CELL = 4;
export class WebglRenderer extends Disposable implements IRenderer { export class WebglRenderer extends Disposable implements IRenderer {
private _renderLayers: IRenderLayer[]; private _renderLayers: IRenderLayer[];
@@ -31,6 +27,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _devicePixelRatio: number; private _devicePixelRatio: number;
private _model: RenderModel = new RenderModel(); private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData();
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
private _gl: IWebGL2RenderingContext; private _gl: IWebGL2RenderingContext;
@@ -40,6 +37,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions; public dimensions: IRenderDimensions;
private _core: ITerminal; private _core: ITerminal;
private _isAttached: boolean;
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
constructor( constructor(
private _terminal: Terminal, private _terminal: Terminal,
@@ -50,11 +51,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._core = (<any>this._terminal)._core; this._core = (<any>this._terminal)._core;
this._applyBgLuminanceBasedSelection();
this._renderLayers = [ this._renderLayers = [
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core), new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
new CursorRenderLayer(this._core.screenElement, 3, this._colors) new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows)
]; ];
this.dimensions = { this.dimensions = {
scaledCharWidth: 0, scaledCharWidth: 0,
@@ -91,6 +90,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Update dimensions and acquire char atlas // Update dimensions and acquire char atlas
this.onCharSizeChanged(); this.onCharSizeChanged();
this._isAttached = document.body.contains(this._core.screenElement);
} }
public dispose(): void { public dispose(): void {
@@ -99,20 +100,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
super.dispose(); super.dispose();
} }
private _applyBgLuminanceBasedSelection(): void {
// HACK: This is needed until webgl renderer adds support for selection colors
if (getLuminance(this._colors.background) > 0.5) {
this._colors.selection = { css: '#000', rgba: 255 };
} else {
this._colors.selection = { css: '#fff', rgba: 4294967295 };
}
}
public setColors(colors: IColorSet): void { public setColors(colors: IColorSet): void {
this._colors = colors; this._colors = colors;
this._applyBgLuminanceBasedSelection();
// Clear layers and force a full render // Clear layers and force a full render
this._renderLayers.forEach(l => { this._renderLayers.forEach(l => {
l.setColors(this._terminal, this._colors); l.setColors(this._terminal, this._colors);
@@ -185,8 +174,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode); this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
this._glyphRenderer.updateSelection(this._model, columnSelectMode); this._glyphRenderer.updateSelection(this._model, columnSelectMode);
// TODO: #2102 Should this move to RenderCoordinator? this._onRequestRefreshRows.fire({ start: 0, end: this._terminal.rows - 1 });
this._core.refresh(0, this._terminal.rows - 1);
} }
public onCursorMove(): void { public onCursorMove(): void {
@@ -206,6 +194,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
*/ */
private _refreshCharAtlas(): void { private _refreshCharAtlas(): void {
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) { if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
// Mark as not attached so char atlas gets refreshed on next render
this._isAttached = false;
return; return;
} }
@@ -231,6 +221,16 @@ export class WebglRenderer extends Disposable implements IRenderer {
} }
public renderRows(start: number, end: number): void { public renderRows(start: number, end: number): void {
if (!this._isAttached) {
if (document.body.contains(this._core.screenElement) && (<any>this._core)._charSizeService.width && (<any>this._core)._charSizeService.height) {
this._updateDimensions();
this._refreshCharAtlas();
this._isAttached = true;
} else {
return;
}
}
// Update render layers // Update render layers
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end)); this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
@@ -255,52 +255,34 @@ export class WebglRenderer extends Disposable implements IRenderer {
const line = terminal.buffer.lines.get(row)!; const line = terminal.buffer.lines.get(row)!;
this._model.lineLengths[y] = 0; this._model.lineLengths[y] = 0;
for (let x = 0; x < terminal.cols; x++) { for (let x = 0; x < terminal.cols; x++) {
const charData = line.get(x); line.loadCell(x, this._workCell);
const chars = charData[CHAR_DATA_CHAR_INDEX];
let code = charData[CHAR_DATA_CODE_INDEX]; const chars = this._workCell.getChars();
const attr = getCompatAttr(line, x); // charData[CHAR_DATA_ATTR_INDEX]; let code = this._workCell.getCode();
const i = ((y * terminal.cols) + x) * INDICIES_PER_CELL; const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
if (code !== NULL_CELL_CODE) { if (code !== NULL_CELL_CODE) {
this._model.lineLengths[y] = x + 1; this._model.lineLengths[y] = x + 1;
} }
// Nothing has changed, no updates needed // Nothing has changed, no updates needed
if (this._model.cells[i] === code && this._model.cells[i + 1] === attr) { if (this._model.cells[i] === code &&
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
continue; continue;
} }
// Resolve bg and fg and cache in the model
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 (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
if (fg === DEFAULT_COLOR) {
fg = INVERTED_DEFAULT_COLOR;
}
if (bg === DEFAULT_COLOR) {
bg = INVERTED_DEFAULT_COLOR;
}
}
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && !!(flags & FLAGS.BOLD) && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
fg += drawInBrightColor ? 8 : 0;
// Flag combined chars with a bit mask so they're easily identifiable // Flag combined chars with a bit mask so they're easily identifiable
if (chars.length > 1) { if (chars.length > 1) {
code = code | COMBINED_CHAR_BIT_MASK; code = code | COMBINED_CHAR_BIT_MASK;
} }
this._model.cells[i ] = code; // Cache the results in the model
this._model.cells[i + 1] = attr; this._model.cells[i] = code;
this._model.cells[i + 2] = bg; this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
this._model.cells[i + 3] = fg; this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
this._glyphRenderer.updateCell(x, y, code, attr, bg, fg, chars); this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
} }
} }
this._rectangleRenderer.updateBackgrounds(this._model); this._rectangleRenderer.updateBackgrounds(this._model);
@@ -4,7 +4,7 @@
*/ */
import { ICharAtlasConfig } from './Types'; import { ICharAtlasConfig } from './Types';
import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { Attributes } from 'common/buffer/Constants';
import { Terminal, FontWeight } from 'xterm'; import { Terminal, FontWeight } from 'xterm';
import { IColorSet, IColor } from 'browser/Types'; import { IColorSet, IColor } from 'browser/Types';
@@ -21,9 +21,11 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
cursor: NULL_COLOR, cursor: NULL_COLOR,
cursorAccent: NULL_COLOR, cursorAccent: NULL_COLOR,
selection: NULL_COLOR, selection: NULL_COLOR,
selectionOpaque: NULL_COLOR,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the // For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas. // dynamic character atlas.
ansi: colors.ansi.slice() ansi: colors.ansi.slice(),
contrastCache: colors.contrastCache
}; };
return { return {
devicePixelRatio: window.devicePixelRatio, devicePixelRatio: window.devicePixelRatio,
@@ -34,6 +36,8 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
fontWeight: terminal.getOption('fontWeight') as FontWeight, fontWeight: terminal.getOption('fontWeight') as FontWeight,
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight, fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
allowTransparency: terminal.getOption('allowTransparency'), allowTransparency: terminal.getOption('allowTransparency'),
drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
colors: clonedColors colors: clonedColors
}; };
} }
@@ -52,10 +56,12 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
a.allowTransparency === b.allowTransparency && a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth && a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight && a.scaledCharHeight === b.scaledCharHeight &&
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
a.minimumContrastRatio === b.minimumContrastRatio &&
a.colors.foreground === b.colors.foreground && a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background; a.colors.background === b.colors.background;
} }
export function is256Color(colorCode: number): boolean { export function is256Color(colorCode: number): boolean {
return colorCode < DEFAULT_COLOR; return (colorCode & Attributes.CM_MASK) === Attributes.CM_P16 || (colorCode & Attributes.CM_MASK) === Attributes.CM_P256;
} }
+2
View File
@@ -25,5 +25,7 @@ export interface ICharAtlasConfig {
scaledCharWidth: number; scaledCharWidth: number;
scaledCharHeight: number; scaledCharHeight: number;
allowTransparency: boolean; allowTransparency: boolean;
drawBoldTextInBrightColors: boolean;
minimumContrastRatio: number;
colors: IColorSet; colors: IColorSet;
} }
@@ -4,14 +4,14 @@
*/ */
import { ICharAtlasConfig } from './Types'; import { ICharAtlasConfig } from './Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { DIM_OPACITY } from 'browser/renderer/atlas/Constants';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
import { DEFAULT_COLOR, DEFAULT_ATTR } from 'common/buffer/Constants'; import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
import { is256Color } from './CharAtlasUtils';
import { throwIfFalsy } from '../WebglUtils'; import { throwIfFalsy } from '../WebglUtils';
import { IColor } from 'browser/Types'; import { IColor } from 'browser/Types';
import { FLAGS } from '../Constants';
import { IDisposable } from 'xterm'; import { IDisposable } from 'xterm';
import { AttributeData } from 'common/buffer/AttributeData';
import { toCss, ensureContrastRatioRgba } from 'browser/Color';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes, // In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -68,8 +68,12 @@ export class WebglCharAtlas implements IDisposable {
public hasCanvasChanged = false; public hasCanvasChanged = false;
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 }; private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
private _workAttributeData: AttributeData = new AttributeData();
constructor(document: Document, private _config: ICharAtlasConfig) { constructor(
document: Document,
private _config: ICharAtlasConfig
) {
this.cacheCanvas = document.createElement('canvas'); this.cacheCanvas = document.createElement('canvas');
this.cacheCanvas.width = TEXTURE_WIDTH; this.cacheCanvas.width = TEXTURE_WIDTH;
this.cacheCanvas.height = TEXTURE_HEIGHT; this.cacheCanvas.height = TEXTURE_HEIGHT;
@@ -103,9 +107,11 @@ export class WebglCharAtlas implements IDisposable {
protected _doWarmUp(): void { protected _doWarmUp(): void {
// Pre-fill with ASCII 33-126 // Pre-fill with ASCII 33-126
for (let i = 33; i < 126; i++) { for (let i = 33; i < 126; i++) {
const rasterizedGlyph = this._drawToCache(i, DEFAULT_ATTR, DEFAULT_COLOR, DEFAULT_COLOR); const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
this._cacheMap[i] = { this._cacheMap[i] = {
[DEFAULT_ATTR]: rasterizedGlyph [DEFAULT_COLOR]: {
[DEFAULT_COLOR]: rasterizedGlyph
}
}; };
} }
} }
@@ -123,16 +129,23 @@ export class WebglCharAtlas implements IDisposable {
return false; return false;
} }
public getRasterizedGlyphCombinedChar(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph { public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMapCombined[chars]; let rasterizedGlyphSet = this._cacheMapCombined[chars];
if (!rasterizedGlyphSet) { if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {}; rasterizedGlyphSet = {};
this._cacheMapCombined[chars] = rasterizedGlyphSet; this._cacheMapCombined[chars] = rasterizedGlyphSet;
} }
let rasterizedGlyph = rasterizedGlyphSet[attr]; let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) { if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(chars, attr, bg, fg); rasterizedGlyph = this._drawToCache(chars, bg, fg);
rasterizedGlyphSet[attr] = rasterizedGlyph; if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
} }
return rasterizedGlyph; return rasterizedGlyph;
} }
@@ -140,16 +153,23 @@ export class WebglCharAtlas implements IDisposable {
/** /**
* Gets the glyphs texture coords, drawing the texture if it's not already * Gets the glyphs texture coords, drawing the texture if it's not already
*/ */
public getRasterizedGlyph(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph { public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
let rasterizedGlyphSet = this._cacheMap[code]; let rasterizedGlyphSet = this._cacheMap[code];
if (!rasterizedGlyphSet) { if (!rasterizedGlyphSet) {
rasterizedGlyphSet = {}; rasterizedGlyphSet = {};
this._cacheMap[code] = rasterizedGlyphSet; this._cacheMap[code] = rasterizedGlyphSet;
} }
let rasterizedGlyph = rasterizedGlyphSet[attr]; let rasterizedGlyph: IRasterizedGlyph | undefined;
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
if (rasterizedGlyphSetBg) {
rasterizedGlyph = rasterizedGlyphSetBg[fg];
}
if (!rasterizedGlyph) { if (!rasterizedGlyph) {
rasterizedGlyph = this._drawToCache(code, attr, bg, fg); rasterizedGlyph = this._drawToCache(code, bg, fg);
rasterizedGlyphSet[attr] = rasterizedGlyph; if (!rasterizedGlyphSet[bg]) {
rasterizedGlyphSet[bg] = {};
}
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
} }
return rasterizedGlyph; return rasterizedGlyph;
} }
@@ -161,48 +181,160 @@ export class WebglCharAtlas implements IDisposable {
return this._config.colors.ansi[idx]; return this._config.colors.ansi[idx];
} }
private _getBackgroundColor(bg: number): IColor { private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor {
if (this._config.allowTransparency) { if (this._config.allowTransparency) {
// The background color might have some transparency, so we need to render it as fully // The background color might have some transparency, so we need to render it as fully
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice // transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
// around the anti-aliased edges of the glyph, and it would look too dark. // around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR; return TRANSPARENT_COLOR;
} else if (bg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.foreground;
} else if (is256Color(bg)) {
return this._getColorFromAnsiIndex(bg);
} }
// TODO: Support true color
return this._config.colors.background; switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor);
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(bgColor);
// TODO: This object creation is slow
return {
rgba: bgColor << 8,
css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`
};
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.foreground;
}
return this._config.colors.background;
}
} }
private _getForegroundColor(fg: number): IColor { private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
if (fg === INVERTED_DEFAULT_COLOR) { const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
return this._config.colors.background; if (minimumContrastCss) {
} else if (is256Color(fg)) { return minimumContrastCss;
return this._getColorFromAnsiIndex(fg); }
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).css;
case Attributes.CM_RGB:
const arr = AttributeData.toColorRGB(fgColor);
return toCss(arr[0], arr[1], arr[2]);
case Attributes.CM_DEFAULT:
default:
if (inverse) {
const bg = this._config.colors.background.css;
if (bg.length === 9) {
// Remove bg alpha channel if present
return bg.substr(0, 7);
}
return bg;
}
return this._config.colors.foreground.css;
} }
// TODO: Support true color
return this._config.colors.foreground;
} }
private _drawToCache(code: number, attr: number, bg: number, fg: number): IRasterizedGlyph; private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
private _drawToCache(chars: string, attr: number, bg: number, fg: number): IRasterizedGlyph; switch (bgColorMode) {
private _drawToCache(codeOrChars: number | string, attr: number, bg: number, fg: number): IRasterizedGlyph { case Attributes.CM_P16:
case Attributes.CM_P256:
return this._getColorFromAnsiIndex(bgColor).rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.foreground.rgba;
}
return this._config.colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._getColorFromAnsiIndex(fgColor).rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._config.colors.background.rgba;
}
return this._config.colors.foreground.rgba;
}
}
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined {
if (this._config.minimumContrastRatio === 1) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio);
if (!result) {
this._config.colors.contrastCache.setCss(bg, fg, null);
return undefined;
}
const css = toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
);
this._config.colors.contrastCache.setCss(bg, fg, css);
return css;
}
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
this.hasCanvasChanged = true; this.hasCanvasChanged = true;
const flags = attr >> 18;
const bold = !!(flags & FLAGS.BOLD);
const dim = !!(flags & FLAGS.DIM);
const italic = !!(flags & FLAGS.ITALIC);
this._tmpCtx.save(); this._tmpCtx.save();
this._workAttributeData.fg = fg;
this._workAttributeData.bg = bg;
const bold = !!this._workAttributeData.isBold();
const inverse = !!this._workAttributeData.isInverse();
const dim = !!this._workAttributeData.isDim();
const italic = !!this._workAttributeData.isItalic();
let fgColor = this._workAttributeData.getFgColor();
let fgColorMode = this._workAttributeData.getFgColorMode();
let bgColor = this._workAttributeData.getBgColor();
let bgColorMode = this._workAttributeData.getBgColorMode();
if (inverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
// draw the background // draw the background
const backgroundColor = this._getBackgroundColor(bg); const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of // Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor // transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy'; this._tmpCtx.globalCompositeOperation = 'copy';
@@ -217,7 +349,7 @@ export class WebglCharAtlas implements IDisposable {
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top'; this._tmpCtx.textBaseline = 'top';
this._tmpCtx.fillStyle = this._getForegroundColor(fg).css; this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
// Apply alpha to dim the character // Apply alpha to dim the character
if (dim) { if (dim) {
@@ -398,3 +530,8 @@ function clearColor(imageData: ImageData, color: IColor): boolean {
} }
return isEmpty; return isEmpty;
} }
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
@@ -4,7 +4,6 @@
*/ */
import { IRenderLayer } from './Types'; import { IRenderLayer } from './Types';
import { IGlyphIdentifier } from '../atlas/Types';
import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
@@ -25,19 +24,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected _charAtlas: WebglCharAtlas | undefined; protected _charAtlas: WebglCharAtlas | undefined;
/**
* An object that's reused when drawing glyphs in order to reduce GC.
*/
private _currentGlyphIdentifier: IGlyphIdentifier = {
chars: '',
code: 0,
bg: 0,
fg: 0,
bold: false,
dim: false,
italic: false
};
constructor( constructor(
private _container: HTMLElement, private _container: HTMLElement,
id: string, id: string,
@@ -8,7 +8,8 @@ import { BaseRenderLayer } from './BaseRenderLayer';
import { ICellData } from 'common/Types'; import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { IRenderDimensions } from 'browser/renderer/Types'; import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IEventEmitter } from 'common/EventEmitter';
interface ICursorState { interface ICursorState {
x: number; x: number;
@@ -29,7 +30,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _cursorBlinkStateManager: CursorBlinkStateManager | undefined; private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData(); private _cell: ICellData = new CellData();
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) { constructor(
container: HTMLElement,
zIndex: number,
colors: IColorSet,
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRefreshRowsEvent>
) {
super(container, 'cursor', zIndex, true, colors); super(container, 'cursor', zIndex, true, colors);
this._state = { this._state = {
x: 0, x: 0,
@@ -70,14 +76,14 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._cursorBlinkStateManager) { if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause(); this._cursorBlinkStateManager.pause();
} }
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
} }
public onFocus(terminal: Terminal): void { public onFocus(terminal: Terminal): void {
if (this._cursorBlinkStateManager) { if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume(terminal); this._cursorBlinkStateManager.resume(terminal);
} else { } else {
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY); this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
} }
} }
@@ -89,13 +95,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
}); });
} }
} else { } else {
if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager.dispose(); this._cursorBlinkStateManager = undefined;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
terminal.refresh(terminal.buffer.cursorY, terminal.buffer.cursorY);
} }
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
} }
public onCursorMove(terminal: Terminal): void { public onCursorMove(terminal: Terminal): void {
@@ -115,7 +120,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void { private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden // Don't draw the cursor if it's hidden
// TODO: Need to expose API for this // TODO: Need to expose API for this
if (!(terminal as any)._core.cursorState || (terminal as any)._core.cursorHidden) { if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) {
this._clearCursor(); this._clearCursor();
return; return;
} }
@@ -139,12 +144,17 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor(); this._clearCursor();
this._ctx.save(); this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css; this._ctx.fillStyle = this._colors.cursor.css;
this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell); const cursorStyle = terminal.getOption('cursorStyle');
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
}
this._ctx.restore(); this._ctx.restore();
this._state.x = terminal.buffer.cursorX; this._state.x = terminal.buffer.cursorX;
this._state.y = viewportRelativeCursorY; this._state.y = viewportRelativeCursorY;
this._state.isFocused = false; this._state.isFocused = false;
this._state.style = terminal.getOption('cursorStyle'); this._state.style = cursorStyle;
this._state.width = this._cell.getWidth(); this._state.width = this._cell.getWidth();
return; return;
} }
+57 -36
View File
@@ -7,59 +7,78 @@ jobs:
- job: Linux - job: Linux
pool: pool:
vmImage: 'ubuntu-16.04' vmImage: 'ubuntu-16.04'
variables:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - task: YarnInstaller@3
inputs: inputs:
versionSpec: "1.9.4" versionSpec: '1.x'
displayName: 'Install Yarn' displayName: 'Install Yarn'
- script: | - task: CacheBeta@1
yarn inputs:
key: yarn2 | $(Agent.OS) | yarn.lock
path: node_modules
displayName: Cache node modules
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: |
yarn test-unit yarn test-unit-coverage --forbid-only
./node_modules/.bin/nyc report --reporter=cobertura
displayName: 'Unit tests' displayName: 'Unit tests'
- script: | - script: yarn lint
yarn lint
displayName: 'Lint' displayName: 'Lint'
- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: Cobertura
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/*coverage.xml'
displayName: 'Publish coverage'
- job: macOS - job: macOS
pool: pool:
vmImage: 'xcode9-macos10.13' vmImage: 'xcode9-macos10.13'
variables:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- script: | - task: CacheBeta@1
yarn inputs:
key: yarn2 | $(Agent.OS) | yarn.lock
path: node_modules
displayName: Cache node modules
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: yarn test-unit --forbid-only
yarn test-unit
displayName: 'Unit tests' displayName: 'Unit tests'
- script: | - script: yarn lint
yarn lint
displayName: 'Lint' displayName: 'Lint'
- job: Windows - job: Windows
pool: pool:
vmImage: 'vs2017-win2016' vmImage: 'vs2017-win2016'
variables:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- script: | - task: CacheBeta@1
yarn inputs:
key: yarn2 | $(Agent.OS) | yarn.lock
path: node_modules
displayName: Cache node modules
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: yarn test-unit --forbid-only
yarn test-unit
displayName: 'Unit tests' displayName: 'Unit tests'
- script: | - script: yarn lint
yarn lint
displayName: 'Lint' displayName: 'Lint'
- job: Linux_IntegrationTests - job: Linux_IntegrationTests
@@ -70,17 +89,16 @@ jobs:
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - task: YarnInstaller@3
inputs: inputs:
versionSpec: "1.9.4" versionSpec: '1.x'
displayName: 'Install Yarn' displayName: 'Install Yarn'
- script: | - script: yarn --frozen-lockfile
yarn
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: |
yarn start & yarn start &
sleep 10 sleep 10
yarn test-api --headless yarn test-api --headless --forbid-only
displayName: 'Linux Integration tests' displayName: 'Linux Integration tests'
- job: macOS_IntegrationTests - job: macOS_IntegrationTests
@@ -91,13 +109,12 @@ jobs:
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- script: | - script: yarn --frozen-lockfile
yarn
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: |
yarn start & yarn start &
sleep 10 sleep 10
yarn test-api --headless yarn test-api --headless --forbid-only
displayName: 'MacOS Integration tests' displayName: 'MacOS Integration tests'
- job: Release - job: Release
@@ -107,22 +124,26 @@ jobs:
- Windows - Windows
- Linux_IntegrationTests - Linux_IntegrationTests
- macOS_IntegrationTests - macOS_IntegrationTests
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*'))) condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
pool: pool:
vmImage: 'ubuntu-16.04' vmImage: 'ubuntu-16.04'
variables:
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: 1
steps: steps:
- task: NodeTool@0 - task: NodeTool@0
inputs: inputs:
versionSpec: '8.x' versionSpec: '8.x'
displayName: 'Install Node.js' displayName: 'Install Node.js'
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 - task: YarnInstaller@3
inputs: inputs:
versionSpec: "1.9.4" versionSpec: '1.x'
displayName: 'Install Yarn' displayName: 'Install Yarn'
- script: | - task: CacheBeta@1
yarn inputs:
BUILD_DIR=dist npm run build key: yarn2 | $(Agent.OS) | yarn.lock
path: node_modules
displayName: Cache node modules
- script: yarn --frozen-lockfile
displayName: 'Install dependencies and build' displayName: 'Install dependencies and build'
- script: | - script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js displayName: 'Package and publish to npm'
displayName: 'Publish to npm'
+56
View File
@@ -0,0 +1,56 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* Script to initialize addon packages under "addons/" with outer deps.
*/
const path = require('path');
const cp = require('child_process');
const fs = require('fs');
const PACKAGE_ROOT = path.join(__dirname, '..');
// install addon deps
const addonsPath = path.join(PACKAGE_ROOT, 'addons');
if (fs.existsSync(addonsPath)) {
console.log('pulling addon dependencies...');
// whether to use yarn or npm
let hasYarn = false;
try {
cp.execSync('yarn --version').toString();
hasYarn = true;
} catch(e) {}
// walk all addon folders
fs.readdir(addonsPath, (err, files) => {
files.forEach(folder => {
const addonPath = path.join(addonsPath, folder);
// install only if there are dependencies listed
let packageJson;
try {
packageJson = require(path.join(addonPath, 'package.json'));
} catch (e) {
// swallow as changing branches can leave folders around
}
if (packageJson
&& (
(packageJson.devDependencies && Object.keys(packageJson.devDependencies).length)
|| (packageJson.dependencies && Object.keys(packageJson.dependencies).length)
)
)
{
console.log('Preparing', folder);
if (hasYarn) {
cp.execSync('yarn', {cwd: addonPath});
} else {
cp.execSync('npm install', {cwd: addonPath});
}
} else {
console.log('Skipped', folder);
}
});
});
}
+22 -4
View File
@@ -5,6 +5,7 @@
const cp = require('child_process'); const cp = require('child_process');
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const path = require('path'); const path = require('path');
// Setup auth // Setup auth
@@ -18,8 +19,9 @@ if (isDryRun) {
const changedFiles = getChangedFilesInCommit('HEAD'); const changedFiles = getChangedFilesInCommit('HEAD');
// Publish xterm if any files were changed outside of the addons directory // Publish xterm if any files were changed outside of the addons directory
let isStableRelease = false;
if (changedFiles.some(e => e.search(/^addons\//) === -1)) { if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
checkAndPublishPackage(path.resolve(__dirname, '..')); isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..'));
} }
// Publish addons if any files were changed inside of the addon // Publish addons if any files were changed inside of the addon
@@ -39,6 +41,11 @@ addonPackageDirs.forEach(p => {
} }
}); });
// Publish website if it's a stable release
if (isStableRelease) {
updateWebsite();
}
function checkAndPublishPackage(packageDir) { function checkAndPublishPackage(packageDir) {
const packageJson = require(path.join(packageDir, 'package.json')); const packageJson = require(path.join(packageDir, 'package.json'));
@@ -76,6 +83,8 @@ function checkAndPublishPackage(packageDir) {
} }
console.groupEnd(); console.groupEnd();
return isStableRelease;
} }
function getNextBetaVersion(packageJson) { function getNextBetaVersion(packageJson) {
@@ -88,7 +97,7 @@ function getNextBetaVersion(packageJson) {
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag);
if (publishedVersions.length === 0) { if (publishedVersions.length === 0) {
return `${nextStableVersion}-${tag}1`; return `${nextStableVersion}-${tag}.1`;
} }
const latestPublishedVersion = publishedVersions.sort((a, b) => { const latestPublishedVersion = publishedVersions.sort((a, b) => {
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/))); const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
@@ -96,14 +105,14 @@ function getNextBetaVersion(packageJson) {
return aVersion > bVersion ? -1 : 1; return aVersion > bVersion ? -1 : 1;
})[0]; })[0];
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10); const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
return `${nextStableVersion}-${tag}${latestTagVersion + 1}`; return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
} }
function getPublishedVersions(packageJson, version, tag) { function getPublishedVersions(packageJson, version, tag) {
const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']); const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
const versionsJson = JSON.parse(versionsProcess.stdout); const versionsJson = JSON.parse(versionsProcess.stdout);
if (tag) { if (tag) {
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`))); return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}.[0-9]+`)));
} }
return versionsJson; return versionsJson;
} }
@@ -115,3 +124,12 @@ function getChangedFilesInCommit(commit) {
const changedFiles = output.split('\n').filter(e => e.length > 0); const changedFiles = output.split('\n').filter(e => e.length > 0);
return changedFiles; return changedFiles;
} }
function updateWebsite() {
console.log('Updating website');
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'website-'));
const packageJson = require(path.join(path.resolve(__dirname, '..'), 'package.json'));
if (!isDryRun) {
cp.spawnSync('sh', [path.join(__dirname, 'update-website.sh'), packageJson.version], { cwd, stdio: [process.stdin, process.stdout, process.stderr] });
}
}
+38 -5
View File
@@ -6,6 +6,8 @@
const cp = require('child_process'); const cp = require('child_process');
const path = require('path'); const path = require('path');
const COVERAGE_LINES_THRESHOLD = 60;
// Add `out` to the NODE_PATH so absolute paths can be resolved. // Add `out` to the NODE_PATH so absolute paths can be resolved.
const env = { ...process.env }; const env = { ...process.env };
env.NODE_PATH = path.resolve(__dirname, '../out'); env.NODE_PATH = path.resolve(__dirname, '../out');
@@ -15,15 +17,42 @@ let testFiles = [
'./out/**/*test.js' './out/**/*test.js'
]; ];
// ability to inject particular test files via let flagArgs = [];
// yarn test [testFileA testFileB ...]
if (process.argv.length > 2) { if (process.argv.length > 2) {
testFiles = process.argv.slice(2); const args = process.argv.slice(2);
flagArgs = args.filter(e => e.startsWith('--'));
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
files = args.filter(e => !e.startsWith('--'));
if (files.length) {
testFiles = files;
}
}
const checkCoverage = flagArgs.indexOf('--coverage') >= 0;
if (checkCoverage) {
flagArgs.splice(flagArgs.indexOf('--coverage'), 1);
const executable = npmBinScript('nyc');
const args = ['--check-coverage', `--lines=${COVERAGE_LINES_THRESHOLD}`, npmBinScript('mocha'), ...testFiles, ...flagArgs];
console.info('executable', executable);
console.info('args', args);
const run = cp.spawnSync(
executable,
args,
{
cwd: path.resolve(__dirname, '..'),
env,
stdio: 'inherit'
}
);
process.exit(run.status);
} }
const run = cp.spawnSync( const run = cp.spawnSync(
path.resolve(__dirname, '../node_modules/.bin/mocha'), npmBinScript('mocha'),
testFiles, [...testFiles, ...flagArgs],
{ {
cwd: path.resolve(__dirname, '..'), cwd: path.resolve(__dirname, '..'),
env, env,
@@ -31,4 +60,8 @@ const run = cp.spawnSync(
} }
); );
function npmBinScript(script) {
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script));
}
process.exit(run.status); process.exit(run.status);
+41
View File
@@ -0,0 +1,41 @@
#!/bin/sh
# Name the arguments
VERSION=$1
# Clone docs repo and update the documentation
git clone https://github.com/xtermjs/xtermjs.org
cd xtermjs.org
yarn
./bin/update-docs
# Add changes to index and only proceed if there are changes to commit
touch test-file
git add .
if ! git diff-index --quiet HEAD --; then
# Delete the upstream branch if it exists for some reason
export BRANCH_NAME=update-$VERSION
git branch -D $BRANCH_NAME || true
git push origin :$BRANCH_NAME || true
# Create commit and push it to update-x.y.z
git checkout -b $BRANCH_NAME
git config --global user.name Daniel Imms
git config --global user.email tyriar@tyriar.com
git commit -m 'Update docs for v$VERSION'
git push --set-upstream origin update-4.2.0
git push -f
# Create a PR in the GitHub repo
curl \
-H "Authorization: token $GITHUB_TOKEN" \
-X POST \
-d "{\"title\":\"Update docs for v$VERSION\",\"base\":\"master\",\"head\":\"xtermjs:$BRANCH_NAME\"}" \
https://api.github.com/repos/xtermjs/xtermjs.org/pulls
else
echo "No changes to commit"
fi
+79 -22
View File
@@ -39,13 +39,39 @@ export interface IWindowWithTerminal extends Window {
declare let window: IWindowWithTerminal; declare let window: IWindowWithTerminal;
let term; let term;
let fitAddon: FitAddon;
let searchAddon: SearchAddon;
let protocol; let protocol;
let socketURL; let socketURL;
let socket; let socket;
let pid; let pid;
type AddonType = 'attach' | 'fit' | 'search' | 'web-links' | 'webgl';
interface IDemoAddon<T extends AddonType> {
name: T;
canChange: boolean;
ctor:
T extends 'attach' ? typeof AttachAddon :
T extends 'fit' ? typeof FitAddon :
T extends 'search' ? typeof SearchAddon :
T extends 'web-links' ? typeof WebLinksAddon :
typeof WebglAddon;
instance?:
T extends 'attach' ? AttachAddon :
T extends 'fit' ? FitAddon :
T extends 'search' ? SearchAddon :
T extends 'web-links' ? WebLinksAddon :
T extends 'webgl' ? WebglAddon :
never;
}
const addons: { [T in AddonType]: IDemoAddon<T>} = {
attach: { name: 'attach', ctor: AttachAddon, canChange: false },
fit: { name: 'fit', ctor: FitAddon, canChange: false },
search: { name: 'search', ctor: SearchAddon, canChange: true },
'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true }
};
const terminalContainer = document.getElementById('terminal-container'); const terminalContainer = document.getElementById('terminal-container');
const actionElements = { const actionElements = {
findNext: <HTMLInputElement>document.querySelector('#find-next'), findNext: <HTMLInputElement>document.querySelector('#find-next'),
@@ -92,7 +118,6 @@ if (document.location.pathname === '/test') {
} else { } else {
createTerminal(); createTerminal();
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler); document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
document.getElementById('webgl').addEventListener('click', () => term.loadAddon(new WebglAddon()));
} }
function createTerminal(): void { function createTerminal(): void {
@@ -108,11 +133,12 @@ function createTerminal(): void {
// Load addons // Load addons
const typedTerm = term as TerminalType; const typedTerm = term as TerminalType;
typedTerm.loadAddon(new WebLinksAddon()); addons['web-links'].instance = new WebLinksAddon();
searchAddon = new SearchAddon(); addons.search.instance = new SearchAddon();
typedTerm.loadAddon(searchAddon); addons.fit.instance = new FitAddon();
fitAddon = new FitAddon(); typedTerm.loadAddon(addons['web-links'].instance);
typedTerm.loadAddon(fitAddon); typedTerm.loadAddon(addons.search.instance);
typedTerm.loadAddon(addons.fit.instance);
window.term = term; // Expose `term` to window for debugging purposes window.term = term; // Expose `term` to window for debugging purposes
term.onResize((size: { cols: number, rows: number }) => { term.onResize((size: { cols: number, rows: number }) => {
@@ -129,17 +155,17 @@ function createTerminal(): void {
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/'; socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer); term.open(terminalContainer);
fitAddon.fit(); addons.fit.instance!.fit();
term.focus(); term.focus();
addDomListener(paddingElement, 'change', setPadding); addDomListener(paddingElement, 'change', setPadding);
addDomListener(actionElements.findNext, 'keyup', (e) => { addDomListener(actionElements.findNext, 'keyup', (e) => {
searchAddon.findNext(actionElements.findNext.value, getSearchOptions(e)); addons.search.instance.findNext(actionElements.findNext.value, getSearchOptions(e));
}); });
addDomListener(actionElements.findPrevious, 'keyup', (e) => { addDomListener(actionElements.findPrevious, 'keyup', (e) => {
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions(e)); addons.search.instance.findPrevious(actionElements.findPrevious.value, getSearchOptions(e));
}); });
// fit is called within a setTimeout, cols and rows need this. // fit is called within a setTimeout, cols and rows need this.
@@ -167,15 +193,10 @@ function createTerminal(): void {
} }
function runRealTerminal(): void { function runRealTerminal(): void {
/** addons.attach.instance = new AttachAddon(socket);
* The demo defaults to string transport by default. term.loadAddon(addons.attach.instance);
* To run it with UTF8 binary transport, swap comment on
* the lines below. (Must also be switched in server.js)
*/
term.loadAddon(new AttachAddon(socket));
// term.loadAddon(new AttachAddon(socket, {inputUtf8: true}));
term._initialized = true; term._initialized = true;
initAddons(term);
} }
function runFakeTerminal(): void { function runFakeTerminal(): void {
@@ -184,6 +205,7 @@ function runFakeTerminal(): void {
} }
term._initialized = true; term._initialized = true;
initAddons(term);
term.prompt = () => { term.prompt = () => {
term.write('\r\n$ '); term.write('\r\n$ ');
@@ -228,6 +250,7 @@ function initOptions(term: TerminalType): void {
bellSound: null, bellSound: null,
bellStyle: ['none', 'sound'], bellStyle: ['none', 'sound'],
cursorStyle: ['block', 'underline', 'bar'], cursorStyle: ['block', 'underline', 'bar'],
fastScrollModifier: ['alt', 'ctrl', 'shift', undefined],
fontFamily: null, fontFamily: null,
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
@@ -260,7 +283,7 @@ function initOptions(term: TerminalType): void {
}); });
html += '</div><div class="option-group">'; html += '</div><div class="option-group">';
numberOptions.forEach(o => { numberOptions.forEach(o => {
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`; html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
}); });
html += '</div><div class="option-group">'; html += '</div><div class="option-group">';
Object.keys(stringOptions).forEach(o => { Object.keys(stringOptions).forEach(o => {
@@ -289,7 +312,7 @@ function initOptions(term: TerminalType): void {
console.log('change', o, input.value); console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') { if (o === 'cols' || o === 'rows') {
updateTerminalSize(); updateTerminalSize();
} else if (o === 'lineHeight') { } else if (o === 'lineHeight' || o === 'scrollSensitivity') {
term.setOption(o, parseFloat(input.value)); term.setOption(o, parseFloat(input.value));
updateTerminalSize(); updateTerminalSize();
} else { } else {
@@ -306,6 +329,40 @@ function initOptions(term: TerminalType): void {
}); });
} }
function initAddons(term: TerminalType): void {
const fragment = document.createDocumentFragment();
Object.keys(addons).forEach((name: AddonType) => {
const addon = addons[name];
const checkbox = document.createElement('input') as HTMLInputElement;
checkbox.type = 'checkbox';
checkbox.checked = !!addon.instance;
if (!addon.canChange) {
checkbox.disabled = true;
}
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
addon.instance = new addon.ctor();
term.loadAddon(addon.instance);
} else {
addon.instance!.dispose();
addon.instance = undefined;
}
});
const label = document.createElement('label');
label.classList.add('addon');
if (!addon.canChange) {
label.title = 'This addon is needed for the demo to operate';
}
label.appendChild(checkbox);
label.appendChild(document.createTextNode(name));
const wrapper = document.createElement('div');
wrapper.classList.add('addon');
wrapper.appendChild(label);
fragment.appendChild(wrapper);
});
document.getElementById('addons-container').appendChild(fragment);
}
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void { function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
element.addEventListener(type, handler); element.addEventListener(type, handler);
term._core.register({ dispose: () => element.removeEventListener(type, handler) }); term._core.register({ dispose: () => element.removeEventListener(type, handler) });
@@ -318,5 +375,5 @@ function updateTerminalSize(): void {
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px'; const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width; terminalContainer.style.width = width;
terminalContainer.style.height = height; terminalContainer.style.height = height;
fitAddon.fit(); addons.fit.instance.fit();
} }
+5 -1
View File
@@ -26,6 +26,11 @@
<p>These options can be set in the <code>Terminal</code> constructor or using the <code>Terminal.setOption</code> function.</p> <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 id="options-container"></div>
</div> </div>
<div>
<h3>Addons</h3>
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
<div id="addons-container"></div>
</div>
<div> <div>
<h3>Style</h3> <h3>Style</h3>
<div style="display: inline-block; margin-right: 16px;"> <div style="display: inline-block; margin-right: 16px;">
@@ -36,7 +41,6 @@
<hr/> <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> <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> <button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
<button id="webgl">Enable WebGL Renderer</button>
<script src="dist/client-bundle.js" defer ></script> <script src="dist/client-bundle.js" defer ></script>
</body> </body>
</html> </html>
+12 -14
View File
@@ -3,12 +3,8 @@ var expressWs = require('express-ws');
var os = require('os'); var os = require('os');
var pty = require('node-pty'); var pty = require('node-pty');
/** // Whether to use binary transport.
* Whether to use UTF8 binary transport. const USE_BINARY = os.platform() !== "win32";
* (Must also be switched in client.ts)
*/
const USE_BINARY_UTF8 = false;
function startServer() { function startServer() {
var app = express(); var app = express();
@@ -18,24 +14,26 @@ function startServer() {
logs = {}; logs = {};
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css')); app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png')); app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/logo.png');
});
app.get('/', function(req, res){ app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/index.html'); res.sendFile(__dirname + '/index.html');
}); });
app.get('/test', function(req, res){ app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/test.html'); res.sendFile(__dirname + '/test.html');
}); });
app.get('/style.css', function(req, res){ app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
res.sendFile(__dirname + '/style.css'); res.sendFile(__dirname + '/style.css');
}); });
app.use('/dist', express.static(__dirname + '/dist')); app.use('/dist', express.static(__dirname + '/dist'));
app.use('/src', express.static(__dirname + '/src')); app.use('/src', express.static(__dirname + '/src'));
app.post('/terminals', function (req, res) { app.post('/terminals', (req, res) => {
const env = Object.assign({}, process.env); const env = Object.assign({}, process.env);
env['COLORTERM'] = 'truecolor'; env['COLORTERM'] = 'truecolor';
var cols = parseInt(req.query.cols), var cols = parseInt(req.query.cols),
@@ -46,7 +44,7 @@ function startServer() {
rows: rows || 24, rows: rows || 24,
cwd: env.PWD, cwd: env.PWD,
env: env, env: env,
encoding: USE_BINARY_UTF8 ? null : 'utf8' encoding: USE_BINARY ? null : 'utf8'
}); });
console.log('Created terminal with PID: ' + term.pid); console.log('Created terminal with PID: ' + term.pid);
@@ -59,7 +57,7 @@ function startServer() {
res.end(); res.end();
}); });
app.post('/terminals/:pid/size', function (req, res) { app.post('/terminals/:pid/size', (req, res) => {
var pid = parseInt(req.params.pid), var pid = parseInt(req.params.pid),
cols = parseInt(req.query.cols), cols = parseInt(req.query.cols),
rows = parseInt(req.query.rows), rows = parseInt(req.query.rows),
@@ -108,7 +106,7 @@ function startServer() {
} }
}; };
} }
const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5); const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
term.on('data', function(data) { term.on('data', function(data) {
try { try {
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5 A[5 A[5 A
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5 @[5 @[5 @
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
fghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5'}[5'}[5'}
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijk lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvw
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB[5'~[5'~[5'~
@@ -0,0 +1,25 @@
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijkqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyzAB
+10 -4
View File
@@ -1,7 +1,7 @@
{ {
"name": "xterm", "name": "xterm",
"description": "Full xterm terminal, in your browser", "description": "Full xterm terminal, in your browser",
"version": "4.0.0", "version": "4.3.0",
"main": "lib/xterm.js", "main": "lib/xterm.js",
"style": "css/xterm.css", "style": "css/xterm.css",
"types": "typings/xterm.d.ts", "types": "typings/xterm.d.ts",
@@ -11,13 +11,16 @@
"prepackage": "npm run build", "prepackage": "npm run build",
"package": "webpack", "package": "webpack",
"start": "node demo/start", "start": "node demo/start",
"lint": "tslint 'src/**/*.ts' 'addons/**/*.ts'", "lint": "tslint 'src/**/*.ts' 'addons/*/src/**/*.ts'",
"test": "npm run test-unit", "test": "npm run test-unit",
"posttest": "npm run lint", "posttest": "npm run lint",
"test-api": "mocha \"**/*.api.js\"", "test-api": "mocha \"**/*.api.js\"",
"test-unit": "node ./bin/test.js", "test-unit": "node ./bin/test.js",
"test-unit-coverage": "node ./bin/test.js --coverage",
"build": "tsc -b ./tsconfig.all.json", "build": "tsc -b ./tsconfig.all.json",
"prepare": "npm run build", "prepare": "npm run setup",
"setup": "npm run build",
"presetup": "node ./bin/install-addons.js",
"prepublishOnly": "npm run package", "prepublishOnly": "npm run package",
"watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput", "watch": "tsc -b -w ./tsconfig.all.json --preserveWatchOutput",
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json", "benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
@@ -27,6 +30,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^3.4.34", "@types/chai": "^3.4.34",
"@types/deep-equal": "^1.0.1",
"@types/glob": "^5.0.35", "@types/glob": "^5.0.35",
"@types/jsdom": "11.0.1", "@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33", "@types/mocha": "^2.2.33",
@@ -36,18 +40,20 @@
"@types/webpack": "^4.4.11", "@types/webpack": "^4.4.11",
"@types/ws": "^6.0.1", "@types/ws": "^6.0.1",
"chai": "3.5.0", "chai": "3.5.0",
"deep-equal": "^1.1.0",
"express": "^4.17.1", "express": "^4.17.1",
"express-ws": "^4.0.0", "express-ws": "^4.0.0",
"glob": "^7.0.5", "glob": "^7.0.5",
"jsdom": "^11.11.0", "jsdom": "^11.11.0",
"mocha": "^6.1.4", "mocha": "^6.1.4",
"node-pty": "0.7.6", "node-pty": "0.7.6",
"nyc": "13",
"puppeteer": "^1.15.0", "puppeteer": "^1.15.0",
"source-map-loader": "^0.2.4", "source-map-loader": "^0.2.4",
"ts-loader": "^6.0.4", "ts-loader": "^6.0.4",
"tslint": "^5.18.0", "tslint": "^5.18.0",
"tslint-consistent-codestyle": "^1.13.0", "tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.5", "typescript": "3.7",
"utf8": "^3.0.0", "utf8": "^3.0.0",
"webpack": "^4.35.3", "webpack": "^4.35.3",
"webpack-cli": "^3.1.0", "webpack-cli": "^3.1.0",
+82 -7
View File
@@ -25,6 +25,14 @@ function getCursor(term: TestTerminal): number[] {
]; ];
} }
function getLines(term: TestTerminal, limit: number = term.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
res.push(term.buffer.lines.get(i).translateToString(true));
}
return res;
}
describe('InputHandler', () => { describe('InputHandler', () => {
describe('save and restore cursor', () => { describe('save and restore cursor', () => {
const terminal = new MockInputHandlingTerminal(); const terminal = new MockInputHandlingTerminal();
@@ -1125,13 +1133,6 @@ describe('InputHandler', () => {
beforeEach(() => { beforeEach(() => {
term = new TestTerminal({cols: 10, rows: 10}); term = new TestTerminal({cols: 10, rows: 10});
}); });
function getLines(term: TestTerminal, limit: number = term.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
res.push(term.buffer.lines.get(i).translateToString(true));
}
return res;
}
it('scrollUp', () => { it('scrollUp', () => {
term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm'); term.writeSync('0\r\n1\r\n2\r\n3\r\n4\r\n5\r\n6\r\n7\r\n8\r\n9\x1b[2;4r\x1b[2Sm');
assert.deepEqual(getLines(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']); assert.deepEqual(getLines(term), ['m', '3', '', '', '4', '5', '6', '7', '8', '9']);
@@ -1191,4 +1192,78 @@ describe('InputHandler', () => {
assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); assert.deepEqual(getLines(term), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']);
}); });
}); });
describe('SL/SR/DECIC/DECDC', () => {
let term: TestTerminal;
beforeEach(() => {
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
});
it('SL (scrollLeft)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']);
term.writeSync('\x1b[0 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']);
term.writeSync('\x1b[2 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
term.writeSync('\x1b[0 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
term.writeSync('\x1b[2 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
});
});
it('should parse big chunks in smaller subchunks', () => {
// max single chunk size is hardcoded as 131072
const calls: any[] = [];
const term = new TestTerminal({cols: 10, rows: 10});
(term as any)._inputHandler._parser.parse = (data: Uint32Array, length: number) => {
calls.push([data.length, length]);
};
term.writeSync('12345');
term.writeSync('a'.repeat(10000));
term.writeSync('a'.repeat(200000));
term.writeSync('a'.repeat(300000));
assert.deepEqual(calls, [
[4096, 5],
[10000, 10000],
[131072, 131072], [131072, 200000 - 131072],
[131072, 131072], [131072, 131072], [131072, 300000 - 131072 - 131072]
]);
});
}); });
+150 -42
View File
@@ -28,6 +28,11 @@ import { DcsHandler } from 'common/parser/DcsParser';
*/ */
const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2}; const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1, '.': 2};
/**
* Max length of the UTF32 input buffer. Real memory consumption is 4 times higher.
*/
const MAX_PARSEBUFFER_LENGTH = 131072;
/** /**
* DCS subparser implementations * DCS subparser implementations
@@ -175,7 +180,9 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI handler * CSI handler
*/ */
this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params)); this._parser.setCsiHandler({final: '@'}, params => this.insertChars(params));
this._parser.setCsiHandler({intermediates: ' ', final: '@'}, params => this.scrollLeft(params));
this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params)); this._parser.setCsiHandler({final: 'A'}, params => this.cursorUp(params));
this._parser.setCsiHandler({intermediates: ' ', final: 'A'}, params => this.scrollRight(params));
this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params)); this._parser.setCsiHandler({final: 'B'}, params => this.cursorDown(params));
this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params)); this._parser.setCsiHandler({final: 'C'}, params => this.cursorForward(params));
this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params)); this._parser.setCsiHandler({final: 'D'}, params => this.cursorBackward(params));
@@ -216,6 +223,8 @@ export class InputHandler extends Disposable implements IInputHandler {
this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params)); this._parser.setCsiHandler({final: 'r'}, params => this.setScrollRegion(params));
this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params)); this._parser.setCsiHandler({final: 's'}, params => this.saveCursor(params));
this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params)); this._parser.setCsiHandler({final: 'u'}, params => this.restoreCursor(params));
this._parser.setCsiHandler({intermediates: '\'', final: '}'}, params => this.insertColumns(params));
this._parser.setCsiHandler({intermediates: '\'', final: '~'}, params => this.deleteColumns(params));
/** /**
* execute handler * execute handler
@@ -324,40 +333,46 @@ export class InputHandler extends Disposable implements IInputHandler {
super.dispose(); super.dispose();
} }
public parse(data: string): void { public parse(data: string | Uint8Array): void {
let buffer = this._bufferService.buffer; let buffer = this._bufferService.buffer;
const cursorStartX = buffer.x; const cursorStartX = buffer.x;
const cursorStartY = buffer.y; const cursorStartY = buffer.y;
this._logService.debug('parsing data', data); this._logService.debug('parsing data', data);
// resize input buffer if needed
if (this._parseBuffer.length < data.length) { if (this._parseBuffer.length < data.length) {
this._parseBuffer = new Uint32Array(data.length); if (this._parseBuffer.length < MAX_PARSEBUFFER_LENGTH) {
this._parseBuffer = new Uint32Array(Math.min(data.length, MAX_PARSEBUFFER_LENGTH));
}
}
// Clear the dirty row service so we know which lines changed as a result of parsing
this._dirtyRowService.clearRange();
// process big data in smaller chunks
if (data.length > MAX_PARSEBUFFER_LENGTH) {
for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
const len = (typeof data === 'string')
? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
: this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
}
} else {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
} }
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
buffer = this._bufferService.buffer; buffer = this._bufferService.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._onCursorMove.fire(); this._onCursorMove.fire();
} }
}
public parseUtf8(data: Uint8Array): void { // Refresh any dirty rows accumulated as part of parsing
let buffer = this._bufferService.buffer; this._terminal.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
this._logService.debug('parsing data', data);
if (this._parseBuffer.length < data.length) {
this._parseBuffer = new Uint32Array(data.length);
}
this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer));
buffer = this._bufferService.buffer;
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
this._onCursorMove.fire();
}
} }
public print(data: Uint32Array, start: number, end: number): void { public print(data: Uint32Array, start: number, end: number): void {
@@ -531,6 +546,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// make buffer local for faster access // make buffer local for faster access
const buffer = this._bufferService.buffer; const buffer = this._bufferService.buffer;
this._dirtyRowService.markDirty(buffer.y);
if (this._optionsService.options.convertEol) { if (this._optionsService.options.convertEol) {
buffer.x = 0; buffer.x = 0;
} }
@@ -545,6 +561,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (buffer.x >= this._bufferService.cols) { if (buffer.x >= this._bufferService.cols) {
buffer.x--; buffer.x--;
} }
this._dirtyRowService.markDirty(buffer.y);
this._onLineFeed.fire(); this._onLineFeed.fire();
} }
@@ -609,12 +626,14 @@ export class InputHandler extends Disposable implements IInputHandler {
this._bufferService.buffer.y = this._terminal.originMode this._bufferService.buffer.y = this._terminal.originMode
? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y)) ? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y)); : Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y));
this._dirtyRowService.markDirty(this._bufferService.buffer.y);
} }
/** /**
* Set absolute cursor position. * Set absolute cursor position.
*/ */
private _setCursor(x: number, y: number): void { private _setCursor(x: number, y: number): void {
this._dirtyRowService.markDirty(this._bufferService.buffer.y);
if (this._terminal.originMode) { if (this._terminal.originMode) {
this._bufferService.buffer.x = x; this._bufferService.buffer.x = x;
this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y; this._bufferService.buffer.y = this._bufferService.buffer.scrollTop + y;
@@ -623,6 +642,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._bufferService.buffer.y = y; this._bufferService.buffer.y = y;
} }
this._restrictCursor(); this._restrictCursor();
this._dirtyRowService.markDirty(this._bufferService.buffer.y);
} }
/** /**
@@ -1046,18 +1066,108 @@ export class InputHandler extends Disposable implements IInputHandler {
* CSI Ps T Scroll down Ps lines (default = 1) (SD). * CSI Ps T Scroll down Ps lines (default = 1) (SD).
*/ */
public scrollDown(params: IParams): void { public scrollDown(params: IParams): void {
if (params.length < 2) { let param = params.params[0] || 1;
let param = params.params[0] || 1;
// make buffer local for faster access // make buffer local for faster access
const buffer = this._bufferService.buffer; const buffer = this._bufferService.buffer;
while (param--) { while (param--) {
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1); buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(this._terminal.eraseAttrData())); buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
} }
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
/**
* CSI Ps SP @ Scroll left Ps columns (default = 1) (SL) ECMA-48
*
* Notation: (Pn)
* Representation: CSI Pn 02/00 04/00
* Parameter default value: Pn = 1
* SL causes the data in the presentation component to be moved by n character positions
* if the line orientation is horizontal, or by n line positions if the line orientation
* is vertical, such that the data appear to move to the left; where n equals the value of Pn.
* The active presentation position is not affected by this control function.
*
* Supported:
* - always left shift (no line orientation setting respected)
*/
public scrollLeft(params: IParams): void {
const buffer = this._bufferService.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const param = params.params[0] || 1;
for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
const line = buffer.lines.get(buffer.ybase + y);
line.deleteCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData()));
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
/**
* CSI Ps SP A Scroll right Ps columns (default = 1) (SR) ECMA-48
*
* Notation: (Pn)
* Representation: CSI Pn 02/00 04/01
* Parameter default value: Pn = 1
* SR causes the data in the presentation component to be moved by n character positions
* if the line orientation is horizontal, or by n line positions if the line orientation
* is vertical, such that the data appear to move to the right; where n equals the value of Pn.
* The active presentation position is not affected by this control function.
*
* Supported:
* - always right shift (no line orientation setting respected)
*/
public scrollRight(params: IParams): void {
const buffer = this._bufferService.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const param = params.params[0] || 1;
for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
const line = buffer.lines.get(buffer.ybase + y);
line.insertCells(0, param, buffer.getNullCell(this._terminal.eraseAttrData()));
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
/**
* CSI Pm ' }
* Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.
*/
public insertColumns(params: IParams): void {
const buffer = this._bufferService.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const param = params.params[0] || 1;
for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
const line = this._bufferService.buffer.lines.get(buffer.ybase + y);
line.insertCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData()));
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
}
/**
* CSI Pm ' ~
* Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.
*/
public deleteColumns(params: IParams): void {
const buffer = this._bufferService.buffer;
if (buffer.y > buffer.scrollBottom || buffer.y < buffer.scrollTop) {
return;
}
const param = params.params[0] || 1;
for (let y = buffer.scrollTop; y <= buffer.scrollBottom; ++y) {
const line = buffer.lines.get(buffer.ybase + y);
line.deleteCells(buffer.x, param, buffer.getNullCell(this._terminal.eraseAttrData()));
line.isWrapped = false;
}
this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom);
} }
/** /**
@@ -1334,19 +1444,17 @@ export class InputHandler extends Disposable implements IInputHandler {
// focusout: ^[[O // focusout: ^[[O
this._terminal.sendFocus = true; this._terminal.sendFocus = true;
break; break;
case 1005: // utf8 ext mode mouse case 1005: // utf8 ext mode mouse - removed in #2507
// for wide terminals this._logService.debug('DECSET 1005 not supported (see #2507)');
// simply encodes large values as utf8 characters
this._coreMouseService.activeEncoding = 'UTF8';
break; break;
case 1006: // sgr ext mode mouse case 1006: // sgr ext mode mouse
this._coreMouseService.activeEncoding = 'SGR'; this._coreMouseService.activeEncoding = 'SGR';
break; break;
case 1015: // urxvt ext mode mouse case 1015: // urxvt ext mode mouse - removed in #2507
this._coreMouseService.activeEncoding = 'URXVT'; this._logService.debug('DECSET 1015 not supported (see #2507)');
break; break;
case 25: // show cursor case 25: // show cursor
this._terminal.cursorHidden = false; this._coreService.isCursorHidden = false;
break; break;
case 1048: // alt screen cursor case 1048: // alt screen cursor
this.saveCursor(); this.saveCursor();
@@ -1507,17 +1615,17 @@ export class InputHandler extends Disposable implements IInputHandler {
case 1004: // send focusin/focusout events case 1004: // send focusin/focusout events
this._terminal.sendFocus = false; this._terminal.sendFocus = false;
break; break;
case 1005: // utf8 ext mode mouse case 1005: // utf8 ext mode mouse - removed in #2507
this._coreMouseService.activeEncoding = 'DEFAULT'; this._logService.debug('DECRST 1005 not supported (see #2507)');
break; break;
case 1006: // sgr ext mode mouse case 1006: // sgr ext mode mouse
this._coreMouseService.activeEncoding = 'DEFAULT'; this._coreMouseService.activeEncoding = 'DEFAULT';
break; break;
case 1015: // urxvt ext mode mouse case 1015: // urxvt ext mode mouse - removed in #2507
this._coreMouseService.activeEncoding = 'DEFAULT'; this._logService.debug('DECRST 1015 not supported (see #2507)');
break; break;
case 25: // hide cursor case 25: // hide cursor
this._terminal.cursorHidden = true; this._coreService.isCursorHidden = true;
break; break;
case 1048: // alt screen cursor case 1048: // alt screen cursor
this.restoreCursor(); this.restoreCursor();
@@ -1851,7 +1959,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* http://vt100.net/docs/vt220-rm/table4-10.html * http://vt100.net/docs/vt220-rm/table4-10.html
*/ */
public softReset(params: IParams): void { public softReset(params: IParams): void {
this._terminal.cursorHidden = false; this._coreService.isCursorHidden = false;
this._terminal.insertMode = false; this._terminal.insertMode = false;
this._terminal.originMode = false; this._terminal.originMode = false;
this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false this._terminal.wraparoundMode = true; // defaults: xterm - true, vt100 - false
+37 -53
View File
@@ -29,11 +29,6 @@ describe('Terminal', () => {
(<any>term).renderer = new MockRenderer(); (<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport(); 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).element = { (<any>term).element = {
classList: { classList: {
toggle: () => { }, toggle: () => { },
@@ -59,18 +54,18 @@ describe('Terminal', () => {
// }); // });
it('should fire the onCursorMove event', (done) => { it('should fire the onCursorMove event', (done) => {
term.onCursorMove(() => done()); term.onCursorMove(() => done());
term.write('foo'); term.writeSync('foo');
}); });
it('should fire the onLineFeed event', (done) => { it('should fire the onLineFeed event', (done) => {
term.onLineFeed(() => done()); term.onLineFeed(() => done());
term.write('\n'); term.writeSync('\n');
}); });
it('should fire a scroll event when scrollback is created', (done) => { it('should fire a scroll event when scrollback is created', (done) => {
term.onScroll(() => done()); term.onScroll(() => done());
term.write('\n'.repeat(INIT_ROWS)); term.writeSync('\n'.repeat(INIT_ROWS));
}); });
it('should fire a scroll event when scrollback is cleared', (done) => { it('should fire a scroll event when scrollback is cleared', (done) => {
term.write('\n'.repeat(INIT_ROWS)); term.writeSync('\n'.repeat(INIT_ROWS));
term.onScroll(() => done()); term.onScroll(() => done());
term.clear(); term.clear();
}); });
@@ -168,17 +163,6 @@ describe('Terminal', () => {
}); });
}); });
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', () => { describe('clear', () => {
it('should clear a buffer equal to rows', () => { it('should clear a buffer equal to rows', () => {
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -195,7 +179,7 @@ describe('Terminal', () => {
it('should clear a buffer larger than rows', () => { it('should clear a buffer larger than rows', () => {
// Fill the buffer with dummy rows // Fill the buffer with dummy rows
for (let i = 0; i < term.rows * 2; i++) { for (let i = 0; i < term.rows * 2; i++) {
term.write('test\n'); term.writeSync('test\n');
} }
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y); const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -244,7 +228,7 @@ describe('Terminal', () => {
assert.equal(e, '\x1b[200~foo\x1b[201~'); assert.equal(e, '\x1b[200~foo\x1b[201~');
done(); done();
}); });
term.write('\x1b[?2004h'); term.writeSync('\x1b[?2004h');
term.paste('foo'); term.paste('foo');
}); });
}); });
@@ -254,7 +238,7 @@ describe('Terminal', () => {
let startYDisp: number; let startYDisp: number;
beforeEach(() => { beforeEach(() => {
for (let i = 0; i < INIT_ROWS * 2; i++) { for (let i = 0; i < INIT_ROWS * 2; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
startYDisp = INIT_ROWS + 1; startYDisp = INIT_ROWS + 1;
}); });
@@ -289,7 +273,7 @@ describe('Terminal', () => {
let startYDisp: number; let startYDisp: number;
beforeEach(() => { beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) { for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
startYDisp = (term.rows * 2) + 1; startYDisp = (term.rows * 2) + 1;
}); });
@@ -312,7 +296,7 @@ describe('Terminal', () => {
describe('scrollToTop', () => { describe('scrollToTop', () => {
beforeEach(() => { beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) { for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
}); });
it('should scroll to the top', () => { it('should scroll to the top', () => {
@@ -326,7 +310,7 @@ describe('Terminal', () => {
let startYDisp: number; let startYDisp: number;
beforeEach(() => { beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) { for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
startYDisp = (term.rows * 2) + 1; startYDisp = (term.rows * 2) + 1;
}); });
@@ -347,7 +331,7 @@ describe('Terminal', () => {
let startYDisp: number; let startYDisp: number;
beforeEach(() => { beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) { for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
startYDisp = (term.rows * 2) + 1; startYDisp = (term.rows * 2) + 1;
}); });
@@ -392,7 +376,7 @@ describe('Terminal', () => {
it('should not scroll down, when a custom keydown handler prevents the event', () => { it('should not scroll down, when a custom keydown handler prevents the event', () => {
// Add some output to the terminal // Add some output to the terminal
for (let i = 0; i < term.rows * 3; i++) { for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test'); term.writeSync('test\r\n');
} }
const startYDisp = (term.rows * 2) + 1; const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => { term.attachCustomKeyEventHandler(() => {
@@ -737,7 +721,7 @@ describe('Terminal', () => {
const high = String.fromCharCode(0xD800); const high = String.fromCharCode(0xD800);
const cell = new CellData(); const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high + String.fromCharCode(i)); term.writeSync(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).loadCell(0, cell); const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2); expect(tchar.getChars().length).eql(2);
@@ -751,7 +735,7 @@ describe('Terminal', () => {
const cell = new CellData(); const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1; term.buffer.x = term.cols - 1;
term.write(high + String.fromCharCode(i)); term.writeSync(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2); expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(''); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
@@ -764,7 +748,7 @@ describe('Terminal', () => {
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1; term.buffer.x = term.cols - 1;
term.wraparoundMode = true; term.wraparoundMode = true;
term.write('a' + high + String.fromCharCode(i)); term.writeSync('a' + high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2); expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
@@ -782,7 +766,7 @@ describe('Terminal', () => {
if (width !== 1) { if (width !== 1) {
continue; continue;
} }
term.write('a' + high + String.fromCharCode(i)); term.writeSync('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line // auto wraparound mode should cut off the rest of the line
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i)); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql(high + String.fromCharCode(i));
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2); expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(2);
@@ -794,8 +778,8 @@ describe('Terminal', () => {
const high = String.fromCharCode(0xD800); const high = String.fromCharCode(0xD800);
const cell = new CellData(); const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) { for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.write(high); term.writeSync(high);
term.write(String.fromCharCode(i)); term.writeSync(String.fromCharCode(i));
const tchar = term.buffer.lines.get(0).loadCell(0, cell); const tchar = term.buffer.lines.get(0).loadCell(0, cell);
expect(tchar.getChars()).eql(high + String.fromCharCode(i)); expect(tchar.getChars()).eql(high + String.fromCharCode(i));
expect(tchar.getChars().length).eql(2); expect(tchar.getChars().length).eql(2);
@@ -809,7 +793,7 @@ describe('Terminal', () => {
describe('unicode - combining characters', () => { describe('unicode - combining characters', () => {
const cell = new CellData(); const cell = new CellData();
it('café', () => { it('café', () => {
term.write('cafe\u0301'); term.writeSync('cafe\u0301');
term.buffer.lines.get(0).loadCell(3, cell); term.buffer.lines.get(0).loadCell(3, cell);
expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2); expect(cell.getChars().length).eql(2);
@@ -817,7 +801,7 @@ describe('Terminal', () => {
}); });
it('café - end of line', () => { it('café - end of line', () => {
term.buffer.x = term.cols - 1 - 3; term.buffer.x = term.cols - 1 - 3;
term.write('cafe\u0301'); term.writeSync('cafe\u0301');
term.buffer.lines.get(0).loadCell(term.cols - 1, cell); term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars()).eql('e\u0301');
expect(cell.getChars().length).eql(2); expect(cell.getChars().length).eql(2);
@@ -829,7 +813,7 @@ describe('Terminal', () => {
}); });
it('multiple combined é', () => { it('multiple combined é', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(100).join('e\u0301')); term.writeSync(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('e\u0301'); expect(cell.getChars()).eql('e\u0301');
@@ -843,7 +827,7 @@ describe('Terminal', () => {
}); });
it('multiple surrogate with combined', () => { it('multiple surrogate with combined', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(100).join('\uD800\uDC00\u0301')); term.writeSync(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
expect(cell.getChars()).eql('\uD800\uDC00\u0301'); expect(cell.getChars()).eql('\uD800\uDC00\u0301');
@@ -861,18 +845,18 @@ describe('Terminal', () => {
const cell = new CellData(); const cell = new CellData();
it('cursor movement even', () => { it('cursor movement even', () => {
expect(term.buffer.x).eql(0); expect(term.buffer.x).eql(0);
term.write('¥'); term.writeSync('¥');
expect(term.buffer.x).eql(2); expect(term.buffer.x).eql(2);
}); });
it('cursor movement odd', () => { it('cursor movement odd', () => {
term.buffer.x = 1; term.buffer.x = 1;
expect(term.buffer.x).eql(1); expect(term.buffer.x).eql(1);
term.write('¥'); term.writeSync('¥');
expect(term.buffer.x).eql(3); expect(term.buffer.x).eql(3);
}); });
it('line of ¥ even', () => { it('line of ¥ even', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('¥')); term.writeSync(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) { if (i % 2) {
@@ -893,7 +877,7 @@ describe('Terminal', () => {
it('line of ¥ odd', () => { it('line of ¥ odd', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('¥')); term.writeSync(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) { if (!(i % 2)) {
@@ -918,7 +902,7 @@ describe('Terminal', () => {
it('line of ¥ with combining odd', () => { it('line of ¥ with combining odd', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('¥\u0301')); term.writeSync(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) { if (!(i % 2)) {
@@ -942,7 +926,7 @@ describe('Terminal', () => {
}); });
it('line of ¥ with combining even', () => { it('line of ¥ with combining even', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('¥\u0301')); term.writeSync(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) { if (i % 2) {
@@ -963,7 +947,7 @@ describe('Terminal', () => {
it('line of surrogate fullwidth with combining odd', () => { it('line of surrogate fullwidth with combining odd', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.buffer.x = 1; term.buffer.x = 1;
term.write(Array(50).join('\ud843\ude6d\u0301')); term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) { for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (!(i % 2)) { if (!(i % 2)) {
@@ -987,7 +971,7 @@ describe('Terminal', () => {
}); });
it('line of surrogate fullwidth with combining even', () => { it('line of surrogate fullwidth with combining even', () => {
term.wraparoundMode = true; term.wraparoundMode = true;
term.write(Array(50).join('\ud843\ude6d\u0301')); term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) { for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0).loadCell(i, cell); term.buffer.lines.get(0).loadCell(i, cell);
if (i % 2) { if (i % 2) {
@@ -1010,11 +994,11 @@ describe('Terminal', () => {
describe('insert mode', () => { describe('insert mode', () => {
const cell = new CellData(); const cell = new CellData();
it('halfwidth - all', () => { it('halfwidth - all', () => {
term.write(Array(9).join('0123456789').slice(-80)); term.writeSync(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10; term.buffer.x = 10;
term.buffer.y = 0; term.buffer.y = 0;
term.insertMode = true; term.insertMode = true;
term.write('abcde'); term.writeSync('abcde');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e'); expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
@@ -1022,11 +1006,11 @@ describe('Terminal', () => {
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4'); expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
}); });
it('fullwidth - insert', () => { it('fullwidth - insert', () => {
term.write(Array(9).join('0123456789').slice(-80)); term.writeSync(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10; term.buffer.x = 10;
term.buffer.y = 0; term.buffer.y = 0;
term.insertMode = true; term.insertMode = true;
term.write('¥¥¥'); term.writeSync('¥¥¥');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql(''); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
@@ -1035,16 +1019,16 @@ describe('Terminal', () => {
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3'); expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
}); });
it('fullwidth - right border', () => { it('fullwidth - right border', () => {
term.write(Array(41).join('¥')); term.writeSync(Array(41).join('¥'));
term.buffer.x = 10; term.buffer.x = 10;
term.buffer.y = 0; term.buffer.y = 0;
term.insertMode = true; term.insertMode = true;
term.write('a'); term.writeSync('a');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a'); expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
term.write('b'); term.writeSync('b');
expect(term.buffer.lines.get(0).length).eql(term.cols); expect(term.buffer.lines.get(0).length).eql(term.cols);
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b'); expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥'); expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
+71 -306
View File
@@ -28,7 +28,7 @@ import { Viewport } from 'browser/Viewport';
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
import { C0 } from 'common/data/EscapeSequences'; import { C0 } from 'common/data/EscapeSequences';
import { InputHandler } from './InputHandler'; import { InputHandler } from './InputHandler';
import { Renderer } from './renderer/Renderer'; import { Renderer } from 'browser/renderer/Renderer';
import { Linkifier } from 'browser/Linkifier'; import { Linkifier } from 'browser/Linkifier';
import { SelectionService } from 'browser/services/SelectionService'; import { SelectionService } from 'browser/services/SelectionService';
import * as Browser from 'common/Platform'; import * as Browser from 'common/Platform';
@@ -38,17 +38,17 @@ import { SoundService } from 'browser/services/SoundService';
import { MouseZoneManager } from 'browser/MouseZoneManager'; import { MouseZoneManager } from 'browser/MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager'; import { AccessibilityManager } from './AccessibilityManager';
import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm';
import { DomRenderer } from './renderer/dom/DomRenderer'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types'; import { IKeyboardEvent, KeyboardResultType, ICharset, IBufferLine, IAttributeData, CoreMouseEventType, CoreMouseButton, CoreMouseAction } from 'common/Types';
import { evaluateKeyboardEvent } from 'common/input/Keyboard'; import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { EventEmitter, IEvent } from 'common/EventEmitter'; import { EventEmitter, IEvent } from 'common/EventEmitter';
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { applyWindowsMode } from './WindowsMode'; import { handleWindowsModeLineFeed } from 'common/WindowsMode';
import { ColorManager } from 'browser/ColorManager'; import { ColorManager } from 'browser/ColorManager';
import { RenderService } from 'browser/services/RenderService'; import { RenderService } from 'browser/services/RenderService';
import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services'; import { IOptionsService, IBufferService, ICoreMouseService, ICoreService, ILogService, IDirtyRowService, IInstantiationService } from 'common/services/Services';
import { OptionsService } from 'common/services/OptionsService'; import { OptionsService } from 'common/services/OptionsService';
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService } from 'browser/services/Services'; import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
import { CharSizeService } from 'browser/services/CharSizeService'; import { CharSizeService } from 'browser/services/CharSizeService';
import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService'; import { BufferService, MINIMUM_COLS, MINIMUM_ROWS } from 'common/services/BufferService';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/Lifecycle';
@@ -62,25 +62,12 @@ import { ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions,
import { DirtyRowService } from 'common/services/DirtyRowService'; import { DirtyRowService } from 'common/services/DirtyRowService';
import { InstantiationService } from 'common/services/InstantiationService'; import { InstantiationService } from 'common/services/InstantiationService';
import { CoreMouseService } from 'common/services/CoreMouseService'; import { CoreMouseService } from 'common/services/CoreMouseService';
import { WriteBuffer } from 'common/input/WriteBuffer';
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
// Let it work inside Node.js for automated testing purposes. // Let it work inside Node.js for automated testing purposes.
const document = (typeof window !== 'undefined') ? window.document : null; const document = (typeof window !== 'undefined') ? window.document : null;
/**
* The amount of write requests to queue before sending an XOFF signal to the
* pty process. This number must be small in order for ^C and similar sequences
* to be responsive.
*/
const WRITE_BUFFER_PAUSE_THRESHOLD = 5;
/**
* The max number of ms to spend on writes before allowing the renderer to
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
* depends on the time it takes for the renderer to draw the frame.
*/
const WRITE_TIMEOUT_MS = 12;
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal { export class Terminal extends Disposable implements ITerminal, IDisposable, IInputHandlingTerminal {
public textarea: HTMLTextAreaElement; public textarea: HTMLTextAreaElement;
@@ -90,7 +77,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
/** /**
* The HTMLElement that the terminal is created in, set by Terminal.open. * The HTMLElement that the terminal is created in, set by Terminal.open.
*/ */
private _parent: HTMLElement; private _parent: HTMLElement | null;
private _document: Document; private _document: Document;
private _viewportScrollArea: HTMLElement; private _viewportScrollArea: HTMLElement;
private _viewportElement: HTMLElement; private _viewportElement: HTMLElement;
@@ -104,10 +91,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// TODO: We should remove options once components adopt optionsService // TODO: We should remove options once components adopt optionsService
public get options(): ITerminalOptions { return this.optionsService.options; } public get options(): ITerminalOptions { return this.optionsService.options; }
// TODO: This can be changed to an enum or boolean, 0 and 1 seem to be the only options
public cursorState: number;
public cursorHidden: boolean;
private _customKeyEventHandler: CustomKeyEventHandler; private _customKeyEventHandler: CustomKeyEventHandler;
// common services // common services
@@ -153,21 +136,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public params: (string | number)[]; public params: (string | number)[];
public currentParam: string | number; public currentParam: string | number;
// user input states // write buffer
public writeBuffer: string[]; private _writeBuffer: WriteBuffer;
public writeBufferUtf8: Uint8Array[];
private _writeInProgress: boolean;
/**
* Whether _xterm.js_ sent XOFF in order to catch up with the pty process.
* This is a distinct state from writeStopped so that if the user requested
* XOFF via ^S that it will not automatically resume when the writeBuffer goes
* below threshold.
*/
private _xoffSentToCatchUp: boolean;
/** Whether writing has been stopped as a result of XOFF */
// private _writeStopped: boolean;
// Store if user went browsing history in scrollback // Store if user went browsing history in scrollback
private _userScrolling: boolean; private _userScrolling: boolean;
@@ -199,6 +169,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; } public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
private _onData = new EventEmitter<string>(); private _onData = new EventEmitter<string>();
public get onData(): IEvent<string> { return this._onData.event; } public get onData(): IEvent<string> { return this._onData.event; }
private _onBinary = new EventEmitter<string>();
public get onBinary(): IEvent<string> { return this._onBinary.event; }
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>(); private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; } public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
private _onLineFeed = new EventEmitter<void>(); private _onLineFeed = new EventEmitter<void>();
@@ -251,6 +223,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom()); this._coreService = this._instantiationService.createInstance(CoreService, () => this.scrollToBottom());
this._instantiationService.setService(ICoreService, this._coreService); this._instantiationService.setService(ICoreService, this._coreService);
this._coreService.onData(e => this._onData.fire(e)); this._coreService.onData(e => this._onData.fire(e));
this._coreService.onBinary(e => this._onBinary.fire(e));
this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); this._coreMouseService = this._instantiationService.createInstance(CoreMouseService);
this._instantiationService.setService(ICoreMouseService, this._coreMouseService); this._instantiationService.setService(ICoreMouseService, this._coreMouseService);
this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService);
@@ -258,6 +231,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._setupOptionsListeners(); this._setupOptionsListeners();
this._setup(); this._setup();
this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
} }
public dispose(): void { public dispose(): void {
@@ -265,25 +240,17 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
return; return;
} }
super.dispose(); super.dispose();
if (this._windowsMode) { this._windowsMode?.dispose();
this._windowsMode.dispose(); this._windowsMode = undefined;
this._windowsMode = undefined; this._renderService?.dispose();
}
if (this._renderService) {
this._renderService.dispose();
}
this._customKeyEventHandler = null; this._customKeyEventHandler = null;
this.write = () => {}; this.write = () => {};
if (this.element && this.element.parentNode) { this.element?.parentNode?.removeChild(this.element);
this.element.parentNode.removeChild(this.element);
}
} }
private _setup(): void { private _setup(): void {
this._parent = document ? document.body : null; this._parent = document ? document.body : null;
this.cursorState = 0;
this.cursorHidden = false;
this._customKeyEventHandler = null; this._customKeyEventHandler = null;
// modes // modes
@@ -306,13 +273,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.params = []; this.params = [];
this.currentParam = 0; this.currentParam = 0;
// user input states
this.writeBuffer = [];
this.writeBufferUtf8 = [];
this._writeInProgress = false;
this._xoffSentToCatchUp = false;
// this._writeStopped = false;
this._userScrolling = false; this._userScrolling = false;
// Register input handler and refire/handle events // Register input handler and refire/handle events
@@ -324,7 +284,13 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService); this.linkifier = this.linkifier || new Linkifier(this._bufferService, this._logService);
if (this.options.windowsMode) { if (this.options.windowsMode) {
this._windowsMode = applyWindowsMode(this); this._enableWindowsMode();
}
}
private _enableWindowsMode(): void {
if (!this._windowsMode) {
this._windowsMode = this.onLineFeed(handleWindowsModeLineFeed.bind(null, this._bufferService));
} }
} }
@@ -357,10 +323,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
} }
} }
public get isFocused(): boolean {
return document.activeElement === this.textarea && document.hasFocus();
}
private _setupOptionsListeners(): void { private _setupOptionsListeners(): void {
// TODO: These listeners should be owned by individual components // TODO: These listeners should be owned by individual components
this.optionsService.onOptionChange(key => { this.optionsService.onOptionChange(key => {
@@ -368,18 +330,15 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
case 'fontFamily': case 'fontFamily':
case 'fontSize': case 'fontSize':
// When the font changes the size of the cells may change which requires a renderer clear // When the font changes the size of the cells may change which requires a renderer clear
if (this._renderService) { this._renderService?.clear();
this._renderService.clear(); this._charSizeService?.measure();
}
if (this._charSizeService) {
this._charSizeService.measure();
}
break; break;
case 'drawBoldTextInBrightColors': case 'drawBoldTextInBrightColors':
case 'letterSpacing': case 'letterSpacing':
case 'lineHeight': case 'lineHeight':
case 'fontWeight': case 'fontWeight':
case 'fontWeightBold': case 'fontWeightBold':
case 'minimumContrastRatio':
// When the font changes the size of the cells may change which requires a renderer clear // When the font changes the size of the cells may change which requires a renderer clear
if (this._renderService) { if (this._renderService) {
this._renderService.clear(); this._renderService.clear();
@@ -395,9 +354,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
break; break;
case 'scrollback': case 'scrollback':
this.buffers.resize(this.cols, this.rows); this.buffers.resize(this.cols, this.rows);
if (this.viewport) { this.viewport?.syncScrollArea();
this.viewport.syncScrollArea();
}
break; break;
case 'screenReaderMode': case 'screenReaderMode':
if (this.optionsService.options.screenReaderMode) { if (this.optionsService.options.screenReaderMode) {
@@ -405,38 +362,20 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._accessibilityManager = new AccessibilityManager(this, this._renderService); this._accessibilityManager = new AccessibilityManager(this, this._renderService);
} }
} else { } else {
if (this._accessibilityManager) { this._accessibilityManager?.dispose();
this._accessibilityManager.dispose(); this._accessibilityManager = null;
this._accessibilityManager = null;
}
} }
break; break;
case 'tabStopWidth': this.buffers.setupTabStops(); break; case 'tabStopWidth': this.buffers.setupTabStops(); break;
case 'theme': case 'theme':
this._setTheme(this.optionsService.options.theme); this._setTheme(this.optionsService.options.theme);
break; break;
case 'scrollback':
const newBufferLength = this.rows + this.optionsService.options.scrollback;
if (this.buffer.lines.length > newBufferLength) {
const amountToTrim = this.buffer.lines.length - newBufferLength;
const needsRefresh = (this.buffer.ydisp - amountToTrim < 0);
this.buffer.lines.trimStart(amountToTrim);
this.buffer.ybase = Math.max(this.buffer.ybase - amountToTrim, 0);
this.buffer.ydisp = Math.max(this.buffer.ydisp - amountToTrim, 0);
if (needsRefresh) {
this.refresh(0, this.rows - 1);
}
}
case 'windowsMode': case 'windowsMode':
if (this.optionsService.options.windowsMode) { if (this.optionsService.options.windowsMode) {
if (!this._windowsMode) { this._enableWindowsMode();
this._windowsMode = applyWindowsMode(this);
}
} else { } else {
if (this._windowsMode) { this._windowsMode?.dispose();
this._windowsMode.dispose(); this._windowsMode = undefined;
this._windowsMode = undefined;
}
} }
break; break;
} }
@@ -552,6 +491,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
throw new Error('Terminal requires a parent element.'); throw new Error('Terminal requires a parent element.');
} }
if (!document.body.contains(parent)) {
this._logService.warn('Terminal.open was called on an element that was not attached to the DOM');
}
this._document = this._parent.ownerDocument; this._document = this._parent.ownerDocument;
// Create main element container // Create main element container
@@ -593,6 +536,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur())); this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
this._helperContainer.appendChild(this.textarea); this._helperContainer.appendChild(this.textarea);
const coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea);
this._instantiationService.setService(ICoreBrowserService, coreBrowserService);
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
this._instantiationService.setService(ICharSizeService, this._charSizeService); this._instantiationService.setService(ICharSizeService, this._charSizeService);
@@ -604,9 +550,10 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// Performance: Add viewport and helper elements from the fragment // Performance: Add viewport and helper elements from the fragment
this.element.appendChild(fragment); this.element.appendChild(fragment);
this._theme = this.options.theme; this._theme = this.options.theme || this._theme;
this.options.theme = null; this.options.theme = undefined;
this._colorManager = new ColorManager(document, this.options.allowTransparency); this._colorManager = new ColorManager(document, this.options.allowTransparency);
this.optionsService.onOptionChange(e => this._colorManager.onOptionsChange(e));
this._colorManager.setTheme(this._theme); this._colorManager.setTheme(this._theme);
const renderer = this._createRenderer(); const renderer = this._createRenderer();
@@ -693,8 +640,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
private _createRenderer(): IRenderer { private _createRenderer(): IRenderer {
switch (this.options.rendererType) { switch (this.options.rendererType) {
case 'canvas': return new Renderer(this._colorManager.colors, this, this._bufferService, this._charSizeService, this.optionsService); case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager.colors, this.screenElement, this.linkifier);
case 'dom': return new DomRenderer(this, this._colorManager.colors, this._charSizeService, this.optionsService); case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager.colors, this.element, this.screenElement, this._viewportElement, this.linkifier);
default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`);
} }
} }
@@ -705,15 +652,9 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
*/ */
private _setTheme(theme: ITheme): void { private _setTheme(theme: ITheme): void {
this._theme = theme; this._theme = theme;
if (this._colorManager) { this._colorManager?.setTheme(theme);
this._colorManager.setTheme(theme); this._renderService?.setColors(this._colorManager.colors);
} this.viewport?.onThemeChange(this._colorManager.colors);
if (this._renderService) {
this._renderService.setColors(this._colorManager.colors);
}
if (this.viewport) {
this.viewport.onThemeChange(this._colorManager.colors);
}
} }
/** /**
@@ -980,9 +921,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* @param end The row to end at (between start and this.rows - 1). * @param end The row to end at (between start and this.rows - 1).
*/ */
public refresh(start: number, end: number): void { public refresh(start: number, end: number): void {
if (this._renderService) { this._renderService?.refreshRows(start, end);
this._renderService.refreshRows(start, end);
}
} }
/** /**
@@ -991,9 +930,7 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* @param end The row to end at (between start and this.rows - 1). * @param end The row to end at (between start and this.rows - 1).
*/ */
private _queueLinkification(start: number, end: number): void { private _queueLinkification(start: number, end: number): void {
if (this.linkifier) { this.linkifier?.linkifyRows(start, end);
this.linkifier.linkifyRows(start, end);
}
} }
/** /**
@@ -1011,8 +948,8 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* Display the cursor element * Display the cursor element
*/ */
public showCursor(): void { public showCursor(): void {
if (!this.cursorState) { if (!this._coreService.isCursorInitialized) {
this.cursorState = 1; this._coreService.isCursorInitialized = true;
this.refresh(this.buffer.y, this.buffer.y); this.refresh(this.buffer.y, this.buffer.y);
} }
} }
@@ -1144,168 +1081,6 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
} }
} }
/**
* Writes raw utf8 bytes to the terminal.
* @param data UintArray with UTF8 bytes to write to the terminal.
*/
public writeUtf8(data: Uint8Array): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
return;
}
// Ignore falsy data values
if (!data) {
return;
}
this.writeBufferUtf8.push(data);
// Send XOFF to pause the pty process if the write buffer becomes too large so
// xterm.js can catch up before more data is sent. This is necessary in order
// to keep signals such as ^C responsive.
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk
this._coreService.triggerDataEvent(C0.DC3);
this._xoffSentToCatchUp = true;
}
if (!this._writeInProgress && this.writeBufferUtf8.length > 0) {
// Kick off a write which will write all data in sequence recursively
this._writeInProgress = true;
// Kick off an async innerWrite so more writes can come in while processing data
setTimeout(() => {
this._innerWriteUtf8();
});
}
}
protected _innerWriteUtf8(bufferOffset: number = 0): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
this.writeBufferUtf8 = [];
}
const startTime = Date.now();
while (this.writeBufferUtf8.length > bufferOffset) {
const data = this.writeBufferUtf8[bufferOffset];
bufferOffset++;
// If XOFF was sent in order to catch up with the pty process, resume it if
// we reached the end of the writeBuffer to allow more data to come in.
if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) {
this._coreService.triggerDataEvent(C0.DC1);
this._xoffSentToCatchUp = false;
}
this._inputHandler.parseUtf8(data);
this.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
if (this.writeBufferUtf8.length > bufferOffset) {
// Allow renderer to catch up before processing the next batch
// trim already processed chunks if we are above threshold
if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
this.writeBufferUtf8 = this.writeBufferUtf8.slice(bufferOffset);
bufferOffset = 0;
}
setTimeout(() => this._innerWriteUtf8(bufferOffset), 0);
} else {
this._writeInProgress = false;
this.writeBufferUtf8 = [];
}
}
/**
* Writes text to the terminal.
* @param data The text to write to the terminal.
*/
public write(data: string): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
return;
}
// Ignore falsy data values (including the empty string)
if (!data) {
return;
}
this.writeBuffer.push(data);
// Send XOFF to pause the pty process if the write buffer becomes too large so
// xterm.js can catch up before more data is sent. This is necessary in order
// to keep signals such as ^C responsive.
if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBuffer.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
// XOFF - stop pty pipe
// XON will be triggered by emulator before processing data chunk
this._coreService.triggerDataEvent(C0.DC3);
this._xoffSentToCatchUp = true;
}
if (!this._writeInProgress && this.writeBuffer.length > 0) {
// Kick off a write which will write all data in sequence recursively
this._writeInProgress = true;
// Kick off an async innerWrite so more writes can come in while processing data
setTimeout(() => {
this._innerWrite();
});
}
}
protected _innerWrite(bufferOffset: number = 0): void {
// Ensure the terminal isn't disposed
if (this._isDisposed) {
this.writeBuffer = [];
}
const startTime = Date.now();
while (this.writeBuffer.length > bufferOffset) {
const data = this.writeBuffer[bufferOffset];
bufferOffset++;
// If XOFF was sent in order to catch up with the pty process, resume it if
// we reached the end of the writeBuffer to allow more data to come in.
if (this._xoffSentToCatchUp && this.writeBuffer.length === bufferOffset) {
this._coreService.triggerDataEvent(C0.DC1);
this._xoffSentToCatchUp = false;
}
this._inputHandler.parse(data);
this.refresh(this._dirtyRowService.start, this._dirtyRowService.end);
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
if (this.writeBuffer.length > bufferOffset) {
// Allow renderer to catch up before processing the next batch
// trim already processed chunks if we are above threshold
if (bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
this.writeBuffer = this.writeBuffer.slice(bufferOffset);
bufferOffset = 0;
}
setTimeout(() => this._innerWrite(bufferOffset), 0);
} else {
this._writeInProgress = false;
this.writeBuffer = [];
}
}
/**
* Writes text to the terminal, followed by a break line character (\n).
* @param data The text to write to the terminal.
*/
public writeln(data: string): void {
this.write(data + '\r\n');
}
public paste(data: string): void { public paste(data: string): void {
paste(data, this.textarea, this.bracketedPasteMode, this._coreService); paste(data, this.textarea, this.bracketedPasteMode, this._coreService);
} }
@@ -1435,24 +1210,18 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
* Clears the current terminal selection. * Clears the current terminal selection.
*/ */
public clearSelection(): void { public clearSelection(): void {
if (this._selectionService) { this._selectionService?.clearSelection();
this._selectionService.clearSelection();
}
} }
/** /**
* Selects all text within the terminal. * Selects all text within the terminal.
*/ */
public selectAll(): void { public selectAll(): void {
if (this._selectionService) { this._selectionService?.selectAll();
this._selectionService.selectAll();
}
} }
public selectLines(start: number, end: number): void { public selectLines(start: number, end: number): void {
if (this._selectionService) { this._selectionService?.selectLines(start, end);
this._selectionService.selectLines(start, end);
}
} }
/** /**
@@ -1659,9 +1428,11 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this._bufferService.resize(x, y); this._bufferService.resize(x, y);
this.buffers.setupTabStops(this.cols); this.buffers.setupTabStops(this.cols);
if (this._charSizeService) { this._charSizeService?.measure();
this._charSizeService.measure();
} // Sync the scroll area to make sure scroll events don't fire and scroll the viewport to an
// invalid location
this.viewport.syncScrollArea(true);
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
this._onResize.fire({ cols: x, rows: y }); this._onResize.fire({ cols: x, rows: y });
@@ -1742,36 +1513,22 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
this.options.cols = this.cols; this.options.cols = this.cols;
const customKeyEventHandler = this._customKeyEventHandler; const customKeyEventHandler = this._customKeyEventHandler;
const inputHandler = this._inputHandler; const inputHandler = this._inputHandler;
const cursorState = this.cursorState;
const writeBuffer = this.writeBuffer;
const writeBufferUtf8 = this.writeBufferUtf8;
const writeInProgress = this._writeInProgress;
const xoffSentToCatchUp = this._xoffSentToCatchUp;
const userScrolling = this._userScrolling; const userScrolling = this._userScrolling;
this._setup(); this._setup();
this._bufferService.reset(); this._bufferService.reset();
this._coreService.reset(); this._coreService.reset();
this._coreMouseService.reset(); this._coreMouseService.reset();
if (this._selectionService) { this._selectionService?.reset();
this._selectionService.reset();
}
// reattach // reattach
this._customKeyEventHandler = customKeyEventHandler; this._customKeyEventHandler = customKeyEventHandler;
this._inputHandler = inputHandler; this._inputHandler = inputHandler;
this.cursorState = cursorState;
this.writeBuffer = writeBuffer;
this.writeBufferUtf8 = writeBufferUtf8;
this._writeInProgress = writeInProgress;
this._xoffSentToCatchUp = xoffSentToCatchUp;
this._userScrolling = userScrolling; this._userScrolling = userScrolling;
// do a full screen refresh // do a full screen refresh
this.refresh(0, this.rows - 1); this.refresh(0, this.rows - 1);
if (this.viewport) { this.viewport?.syncScrollArea();
this.viewport.syncScrollArea();
}
} }
// TODO: Remove cancel function and cancelEvents option // TODO: Remove cancel function and cancelEvents option
@@ -1795,6 +1552,14 @@ export class Terminal extends Disposable implements ITerminal, IDisposable, IInp
// return this.options.bellStyle === 'sound' || // return this.options.bellStyle === 'sound' ||
// this.options.bellStyle === 'both'; // this.options.bellStyle === 'both';
} }
public write(data: string | Uint8Array, callback?: () => void): void {
this._writeBuffer.write(data, callback);
}
public writeSync(data: string | Uint8Array): void {
this._writeBuffer.writeSync(data);
}
} }
/** /**
+7 -2
View File
@@ -34,7 +34,6 @@ if (os.platform() === 'darwin') {
// filter skipFilenames // filter skipFilenames
const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1); const FILES = TESTFILES.filter(value => SKIP_FILES.indexOf(value.split('/').slice(-1)[0]) === -1);
describe('Escape Sequence Files', function(): void { describe('Escape Sequence Files', function(): void {
this.timeout(1000); this.timeout(1000);
@@ -44,6 +43,9 @@ describe('Escape Sequence Files', function(): void {
let customHandler: IDisposable | undefined; let customHandler: IDisposable | undefined;
before(() => { before(() => {
if (process.platform === 'win32') {
return;
}
ptyTerm = (pty as any).open({cols: COLS, rows: ROWS}); ptyTerm = (pty as any).open({cols: COLS, rows: ROWS});
slaveEnd = ptyTerm._slave; slaveEnd = ptyTerm._slave;
term = new Terminal({cols: COLS, rows: ROWS}); term = new Terminal({cols: COLS, rows: ROWS});
@@ -51,12 +53,15 @@ describe('Escape Sequence Files', function(): void {
}); });
after(() => { after(() => {
if (process.platform === 'win32') {
return;
}
ptyTerm._master.end(); ptyTerm._master.end();
ptyTerm._master.destroy(); ptyTerm._master.destroy();
}); });
FILES.forEach(filename => { FILES.forEach(filename => {
it(filename.split('/').slice(-1)[0], async () => { (process.platform === 'win32' ? it.skip : it)(filename.split('/').slice(-1)[0], async () => {
// reset terminal and handler // reset terminal and handler
if (customHandler) { if (customHandler) {
customHandler.dispose(); customHandler.dispose();
+3 -5
View File
@@ -3,7 +3,7 @@
* @license MIT * @license MIT
*/ */
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types'; import { IInputHandlingTerminal, ICompositionHelper, ITerminal, IBrowser, ITerminalOptions } from './Types';
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, CoreMouseEventType } from 'common/Types';
@@ -19,10 +19,6 @@ import { IParams, IFunctionIdentifier } from 'common/parser/Types';
import { ISelectionService } from 'browser/services/Services'; import { ISelectionService } from 'browser/services/Services';
export class TestTerminal extends Terminal { export class TestTerminal extends Terminal {
writeSync(data: string): void {
this.writeBuffer.push(data);
this._innerWrite();
}
keyDown(ev: any): boolean { return this._keyDown(ev); } keyDown(ev: any): boolean { return this._keyDown(ev); }
keyPress(ev: any): boolean { return this._keyPress(ev); } keyPress(ev: any): boolean { return this._keyPress(ev); }
} }
@@ -36,6 +32,7 @@ export class MockTerminal implements ITerminal {
onLineFeed: IEvent<void>; onLineFeed: IEvent<void>;
onSelectionChange: IEvent<void>; onSelectionChange: IEvent<void>;
onData: IEvent<string>; onData: IEvent<string>;
onBinary: IEvent<string>;
onTitleChange: IEvent<string>; onTitleChange: IEvent<string>;
onScroll: IEvent<number>; onScroll: IEvent<number>;
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
@@ -366,6 +363,7 @@ export class MockBuffer implements IBuffer {
} }
export class MockRenderer implements IRenderer { export class MockRenderer implements IRenderer {
onRequestRefreshRows: IEvent<IRequestRefreshRowsEvent>;
onCanvasResize: IEvent<{ width: number; height: number; }>; onCanvasResize: IEvent<{ width: number; height: number; }>;
onRender: IEvent<{ start: number; end: number; }>; onRender: IEvent<{ start: number; end: number; }>;
dispose(): void { dispose(): void {
+9 -12
View File
@@ -38,7 +38,6 @@ export interface IInputHandlingTerminal {
savedCols: number; savedCols: number;
mouseEvents: CoreMouseEventType; mouseEvents: CoreMouseEventType;
sendFocus: boolean; sendFocus: boolean;
cursorHidden: boolean;
buffers: IBufferSet; buffers: IBufferSet;
buffer: IBuffer; buffer: IBuffer;
@@ -73,8 +72,7 @@ export interface ICompositionHelper {
* Calls the parser and handles actions generated by the parser. * Calls the parser and handles actions generated by the parser.
*/ */
export interface IInputHandler { export interface IInputHandler {
parse(data: string): void; parse(data: string | Uint8Array): void;
parseUtf8(data: Uint8Array): void;
print(data: Uint32Array, start: number, end: number): void; print(data: Uint32Array, start: number, end: number): void;
/** C0 BEL */ bell(): void; /** C0 BEL */ bell(): void;
@@ -86,7 +84,9 @@ export interface IInputHandler {
/** C0 SI */ shiftIn(): void; /** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params: IParams): void; /** CSI @ */ insertChars(params: IParams): void;
/** CSI SP @ */ scrollLeft(params: IParams): void;
/** CSI A */ cursorUp(params: IParams): void; /** CSI A */ cursorUp(params: IParams): void;
/** CSI SP A */ scrollRight(params: IParams): void;
/** CSI B */ cursorDown(params: IParams): void; /** CSI B */ cursorDown(params: IParams): void;
/** CSI C */ cursorForward(params: IParams): void; /** CSI C */ cursorForward(params: IParams): void;
/** CSI D */ cursorBackward(params: IParams): void; /** CSI D */ cursorBackward(params: IParams): void;
@@ -122,6 +122,8 @@ export interface IInputHandler {
/** CSI r */ setScrollRegion(params: IParams, collect?: string): void; /** CSI r */ setScrollRegion(params: IParams, collect?: string): void;
/** CSI s */ saveCursor(params: IParams): void; /** CSI s */ saveCursor(params: IParams): void;
/** CSI u */ restoreCursor(params: IParams): void; /** CSI u */ restoreCursor(params: IParams): void;
/** CSI ' } */ insertColumns(params: IParams): void;
/** CSI ' ~ */ deleteColumns(params: IParams): void;
/** OSC 0 /** OSC 0
OSC 2 */ setTitle(data: string): void; OSC 2 */ setTitle(data: string): void;
/** ESC E */ nextLine(): void; /** ESC E */ nextLine(): void;
@@ -151,12 +153,8 @@ export interface IInputHandler {
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor { export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement; screenElement: HTMLElement;
browser: IBrowser; browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
buffer: IBuffer; buffer: IBuffer;
buffers: IBufferSet; buffers: IBufferSet;
isFocused: boolean;
viewport: IViewport; viewport: IViewport;
bracketedPasteMode: boolean; bracketedPasteMode: boolean;
optionsService: IOptionsService; optionsService: IOptionsService;
@@ -175,13 +173,14 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
// Portions of the public API that are required by the internal Terminal // Portions of the public API that are required by the internal Terminal
export interface IPublicTerminal extends IDisposable { export interface IPublicTerminal extends IDisposable {
textarea: HTMLTextAreaElement; textarea: HTMLTextAreaElement | undefined;
rows: number; rows: number;
cols: number; cols: number;
buffer: IBuffer; buffer: IBuffer;
markers: IMarker[]; markers: IMarker[];
onCursorMove: IEvent<void>; onCursorMove: IEvent<void>;
onData: IEvent<string>; onData: IEvent<string>;
onBinary: IEvent<string>;
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>; onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
onLineFeed: IEvent<void>; onLineFeed: IEvent<void>;
onScroll: IEvent<number>; onScroll: IEvent<number>;
@@ -192,7 +191,6 @@ export interface IPublicTerminal extends IDisposable {
blur(): void; blur(): void;
focus(): void; focus(): void;
resize(columns: number, rows: number): void; resize(columns: number, rows: number): void;
writeln(data: string): void;
open(parent: HTMLElement): void; open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable; addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
@@ -218,8 +216,7 @@ export interface IPublicTerminal extends IDisposable {
scrollToBottom(): void; scrollToBottom(): void;
scrollToLine(line: number): void; scrollToLine(line: number): void;
clear(): void; clear(): void;
write(data: string): void; write(data: string | Uint8Array, callback?: () => void): void;
writeUtf8(data: Uint8Array): void;
paste(data: string): void; paste(data: string): void;
refresh(start: number, end: number): void; refresh(start: number, end: number): void;
reset(): void; reset(): void;
@@ -230,7 +227,7 @@ export interface IBufferAccessor {
} }
export interface IElementAccessor { export interface IElementAccessor {
readonly element: HTMLElement; readonly element: HTMLElement | undefined;
} }
export interface ILinkifierAccessor { export interface ILinkifierAccessor {
+6 -6
View File
@@ -82,12 +82,12 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
// Reset the terminal textarea's styling // Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled. // Timeout needs to be long enough for click event to be handled.
setTimeout(() => { setTimeout(() => {
textarea.style.position = null; textarea.style.position = '';
textarea.style.width = null; textarea.style.width = '';
textarea.style.height = null; textarea.style.height = '';
textarea.style.left = null; textarea.style.left = '';
textarea.style.top = null; textarea.style.top = '';
textarea.style.zIndex = null; textarea.style.zIndex = '';
}, 200); }, 200);
} }
+267
View File
@@ -0,0 +1,267 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { blend, fromCss, toPaddedHex, toCss, toRgba, fromRgba, opaque, rgbRelativeLuminance, contrastRatio, ensureContrastRatioRgba } from 'browser/Color';
describe('Color', () => {
describe('blend', () => {
it('should blend colors based on the alpha channel', () => {
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF00', rgba: 0xFFFFFF00 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF10', rgba: 0xFFFFFF10 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF20', rgba: 0xFFFFFF20 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF30', rgba: 0xFFFFFF30 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF40', rgba: 0xFFFFFF40 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF50', rgba: 0xFFFFFF50 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF60', rgba: 0xFFFFFF60 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF70', rgba: 0xFFFFFF70 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF80', rgba: 0xFFFFFF80 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFF90', rgba: 0xFFFFFF90 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFA0', rgba: 0xFFFFFFA0 }), { css: '#a0a0a0', rgba: 0xA0A0A0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFB0', rgba: 0xFFFFFFB0 }), { css: '#b0b0b0', rgba: 0xB0B0B0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFC0', rgba: 0xFFFFFFC0 }), { css: '#c0c0c0', rgba: 0xC0C0C0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFD0', rgba: 0xFFFFFFD0 }), { css: '#d0d0d0', rgba: 0xD0D0D0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFE0', rgba: 0xFFFFFFE0 }), { css: '#e0e0e0', rgba: 0xE0E0E0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFF0', rgba: 0xFFFFFFF0 }), { css: '#f0f0f0', rgba: 0xF0F0F0FF });
assert.deepEqual(blend({ css: '#000000', rgba: 0x000000FF }, { css: '#FFFFFFFF', rgba: 0xFFFFFFFF }), { css: '#FFFFFFFF', rgba: 0xFFFFFFFF });
});
});
describe('fromCss', () => {
it('should covert a CSS string to an IColor', () => {
assert.deepEqual(fromCss('#000000'), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(fromCss('#101010'), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(fromCss('#202020'), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(fromCss('#303030'), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(fromCss('#404040'), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(fromCss('#505050'), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(fromCss('#606060'), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(fromCss('#707070'), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(fromCss('#808080'), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(fromCss('#909090'), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(fromCss('#a0a0a0'), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(fromCss('#b0b0b0'), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(fromCss('#c0c0c0'), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(fromCss('#d0d0d0'), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(fromCss('#e0e0e0'), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(fromCss('#f0f0f0'), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(fromCss('#ffffff'), { css: '#ffffff', rgba: 0xffffffFF });
});
});
describe('toPaddedHex', () => {
it('should convert numbers to 2-digit hex values', () => {
assert.equal(toPaddedHex(0x00), '00');
assert.equal(toPaddedHex(0x10), '10');
assert.equal(toPaddedHex(0x20), '20');
assert.equal(toPaddedHex(0x30), '30');
assert.equal(toPaddedHex(0x40), '40');
assert.equal(toPaddedHex(0x50), '50');
assert.equal(toPaddedHex(0x60), '60');
assert.equal(toPaddedHex(0x70), '70');
assert.equal(toPaddedHex(0x80), '80');
assert.equal(toPaddedHex(0x90), '90');
assert.equal(toPaddedHex(0xa0), 'a0');
assert.equal(toPaddedHex(0xb0), 'b0');
assert.equal(toPaddedHex(0xc0), 'c0');
assert.equal(toPaddedHex(0xd0), 'd0');
assert.equal(toPaddedHex(0xe0), 'e0');
assert.equal(toPaddedHex(0xf0), 'f0');
assert.equal(toPaddedHex(0xff), 'ff');
});
});
describe('toCss', () => {
it('should convert an rgb array to css hex string', () => {
assert.equal(toCss(0x00, 0x00, 0x00), '#000000');
assert.equal(toCss(0x10, 0x10, 0x10), '#101010');
assert.equal(toCss(0x20, 0x20, 0x20), '#202020');
assert.equal(toCss(0x30, 0x30, 0x30), '#303030');
assert.equal(toCss(0x40, 0x40, 0x40), '#404040');
assert.equal(toCss(0x50, 0x50, 0x50), '#505050');
assert.equal(toCss(0x60, 0x60, 0x60), '#606060');
assert.equal(toCss(0x70, 0x70, 0x70), '#707070');
assert.equal(toCss(0x80, 0x80, 0x80), '#808080');
assert.equal(toCss(0x90, 0x90, 0x90), '#909090');
assert.equal(toCss(0xa0, 0xa0, 0xa0), '#a0a0a0');
assert.equal(toCss(0xb0, 0xb0, 0xb0), '#b0b0b0');
assert.equal(toCss(0xc0, 0xc0, 0xc0), '#c0c0c0');
assert.equal(toCss(0xd0, 0xd0, 0xd0), '#d0d0d0');
assert.equal(toCss(0xe0, 0xe0, 0xe0), '#e0e0e0');
assert.equal(toCss(0xf0, 0xf0, 0xf0), '#f0f0f0');
assert.equal(toCss(0xff, 0xff, 0xff), '#ffffff');
});
});
describe('toRgba', () => {
it('should convert an rgb array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00), 0x000000FF);
assert.equal(toRgba(0x10, 0x10, 0x10), 0x101010FF);
assert.equal(toRgba(0x20, 0x20, 0x20), 0x202020FF);
assert.equal(toRgba(0x30, 0x30, 0x30), 0x303030FF);
assert.equal(toRgba(0x40, 0x40, 0x40), 0x404040FF);
assert.equal(toRgba(0x50, 0x50, 0x50), 0x505050FF);
assert.equal(toRgba(0x60, 0x60, 0x60), 0x606060FF);
assert.equal(toRgba(0x70, 0x70, 0x70), 0x707070FF);
assert.equal(toRgba(0x80, 0x80, 0x80), 0x808080FF);
assert.equal(toRgba(0x90, 0x90, 0x90), 0x909090FF);
assert.equal(toRgba(0xa0, 0xa0, 0xa0), 0xa0a0a0FF);
assert.equal(toRgba(0xb0, 0xb0, 0xb0), 0xb0b0b0FF);
assert.equal(toRgba(0xc0, 0xc0, 0xc0), 0xc0c0c0FF);
assert.equal(toRgba(0xd0, 0xd0, 0xd0), 0xd0d0d0FF);
assert.equal(toRgba(0xe0, 0xe0, 0xe0), 0xe0e0e0FF);
assert.equal(toRgba(0xf0, 0xf0, 0xf0), 0xf0f0f0FF);
assert.equal(toRgba(0xff, 0xff, 0xff), 0xffffffFF);
});
it('should convert an rgba array to an rgba number', () => {
assert.equal(toRgba(0x00, 0x00, 0x00, 0x00), 0x00000000);
assert.equal(toRgba(0x10, 0x10, 0x10, 0x10), 0x10101010);
assert.equal(toRgba(0x20, 0x20, 0x20, 0x20), 0x20202020);
assert.equal(toRgba(0x30, 0x30, 0x30, 0x30), 0x30303030);
assert.equal(toRgba(0x40, 0x40, 0x40, 0x40), 0x40404040);
assert.equal(toRgba(0x50, 0x50, 0x50, 0x50), 0x50505050);
assert.equal(toRgba(0x60, 0x60, 0x60, 0x60), 0x60606060);
assert.equal(toRgba(0x70, 0x70, 0x70, 0x70), 0x70707070);
assert.equal(toRgba(0x80, 0x80, 0x80, 0x80), 0x80808080);
assert.equal(toRgba(0x90, 0x90, 0x90, 0x90), 0x90909090);
assert.equal(toRgba(0xa0, 0xa0, 0xa0, 0xa0), 0xa0a0a0a0);
assert.equal(toRgba(0xb0, 0xb0, 0xb0, 0xb0), 0xb0b0b0b0);
assert.equal(toRgba(0xc0, 0xc0, 0xc0, 0xc0), 0xc0c0c0c0);
assert.equal(toRgba(0xd0, 0xd0, 0xd0, 0xd0), 0xd0d0d0d0);
assert.equal(toRgba(0xe0, 0xe0, 0xe0, 0xe0), 0xe0e0e0e0);
assert.equal(toRgba(0xf0, 0xf0, 0xf0, 0xf0), 0xf0f0f0f0);
assert.equal(toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff);
});
});
describe('fromRgba', () => {
it('should convert an rgba number to an rgba array', () => {
assert.deepEqual(fromRgba(0x00000000), [0x00, 0x00, 0x00, 0x00]);
assert.deepEqual(fromRgba(0x10101010), [0x10, 0x10, 0x10, 0x10]);
assert.deepEqual(fromRgba(0x20202020), [0x20, 0x20, 0x20, 0x20]);
assert.deepEqual(fromRgba(0x30303030), [0x30, 0x30, 0x30, 0x30]);
assert.deepEqual(fromRgba(0x40404040), [0x40, 0x40, 0x40, 0x40]);
assert.deepEqual(fromRgba(0x50505050), [0x50, 0x50, 0x50, 0x50]);
assert.deepEqual(fromRgba(0x60606060), [0x60, 0x60, 0x60, 0x60]);
assert.deepEqual(fromRgba(0x70707070), [0x70, 0x70, 0x70, 0x70]);
assert.deepEqual(fromRgba(0x80808080), [0x80, 0x80, 0x80, 0x80]);
assert.deepEqual(fromRgba(0x90909090), [0x90, 0x90, 0x90, 0x90]);
assert.deepEqual(fromRgba(0xa0a0a0a0), [0xa0, 0xa0, 0xa0, 0xa0]);
assert.deepEqual(fromRgba(0xb0b0b0b0), [0xb0, 0xb0, 0xb0, 0xb0]);
assert.deepEqual(fromRgba(0xc0c0c0c0), [0xc0, 0xc0, 0xc0, 0xc0]);
assert.deepEqual(fromRgba(0xd0d0d0d0), [0xd0, 0xd0, 0xd0, 0xd0]);
assert.deepEqual(fromRgba(0xe0e0e0e0), [0xe0, 0xe0, 0xe0, 0xe0]);
assert.deepEqual(fromRgba(0xf0f0f0f0), [0xf0, 0xf0, 0xf0, 0xf0]);
assert.deepEqual(fromRgba(0xffffffff), [0xff, 0xff, 0xff, 0xff]);
});
});
describe('opaque', () => {
it('should make the color opaque', () => {
assert.deepEqual(opaque({ css: '#00000000', rgba: 0x00000000 }), { css: '#000000', rgba: 0x000000FF });
assert.deepEqual(opaque({ css: '#10101010', rgba: 0x10101010 }), { css: '#101010', rgba: 0x101010FF });
assert.deepEqual(opaque({ css: '#20202020', rgba: 0x20202020 }), { css: '#202020', rgba: 0x202020FF });
assert.deepEqual(opaque({ css: '#30303030', rgba: 0x30303030 }), { css: '#303030', rgba: 0x303030FF });
assert.deepEqual(opaque({ css: '#40404040', rgba: 0x40404040 }), { css: '#404040', rgba: 0x404040FF });
assert.deepEqual(opaque({ css: '#50505050', rgba: 0x50505050 }), { css: '#505050', rgba: 0x505050FF });
assert.deepEqual(opaque({ css: '#60606060', rgba: 0x60606060 }), { css: '#606060', rgba: 0x606060FF });
assert.deepEqual(opaque({ css: '#70707070', rgba: 0x70707070 }), { css: '#707070', rgba: 0x707070FF });
assert.deepEqual(opaque({ css: '#80808080', rgba: 0x80808080 }), { css: '#808080', rgba: 0x808080FF });
assert.deepEqual(opaque({ css: '#90909090', rgba: 0x90909090 }), { css: '#909090', rgba: 0x909090FF });
assert.deepEqual(opaque({ css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }), { css: '#a0a0a0', rgba: 0xa0a0a0FF });
assert.deepEqual(opaque({ css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }), { css: '#b0b0b0', rgba: 0xb0b0b0FF });
assert.deepEqual(opaque({ css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }), { css: '#c0c0c0', rgba: 0xc0c0c0FF });
assert.deepEqual(opaque({ css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }), { css: '#d0d0d0', rgba: 0xd0d0d0FF });
assert.deepEqual(opaque({ css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }), { css: '#e0e0e0', rgba: 0xe0e0e0FF });
assert.deepEqual(opaque({ css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }), { css: '#f0f0f0', rgba: 0xf0f0f0FF });
assert.deepEqual(opaque({ css: '#ffffffff', rgba: 0xffffffff }), { css: '#ffffff', rgba: 0xffffffFF });
});
});
describe('rgbRelativeLuminance', () => {
it('should calculate the relative luminance of the color', () => {
assert.equal(rgbRelativeLuminance(0x000000), 0);
assert.equal(rgbRelativeLuminance(0x101010).toFixed(4), '0.0052');
assert.equal(rgbRelativeLuminance(0x202020).toFixed(4), '0.0144');
assert.equal(rgbRelativeLuminance(0x303030).toFixed(4), '0.0296');
assert.equal(rgbRelativeLuminance(0x404040).toFixed(4), '0.0513');
assert.equal(rgbRelativeLuminance(0x505050).toFixed(4), '0.0802');
assert.equal(rgbRelativeLuminance(0x606060).toFixed(4), '0.1170');
assert.equal(rgbRelativeLuminance(0x707070).toFixed(4), '0.1620');
assert.equal(rgbRelativeLuminance(0x808080).toFixed(4), '0.2159');
assert.equal(rgbRelativeLuminance(0x909090).toFixed(4), '0.2789');
assert.equal(rgbRelativeLuminance(0xA0A0A0).toFixed(4), '0.3515');
assert.equal(rgbRelativeLuminance(0xB0B0B0).toFixed(4), '0.4342');
assert.equal(rgbRelativeLuminance(0xC0C0C0).toFixed(4), '0.5271');
assert.equal(rgbRelativeLuminance(0xD0D0D0).toFixed(4), '0.6308');
assert.equal(rgbRelativeLuminance(0xE0E0E0).toFixed(4), '0.7454');
assert.equal(rgbRelativeLuminance(0xF0F0F0).toFixed(4), '0.8714');
assert.equal(rgbRelativeLuminance(0xFFFFFF), 1);
});
});
describe('contrastRatio', () => {
it('should calculate the relative luminance of the color', () => {
assert.equal(contrastRatio(0, 0), 1);
assert.equal(contrastRatio(0, 0.5), 11);
assert.equal(contrastRatio(0, 1), 21);
});
it('should work regardless of the parameter order', () => {
assert.equal(contrastRatio(0, 1), 21);
assert.equal(contrastRatio(1, 0), 21);
});
});
describe('ensureContrastRatioRgba', () => {
it('should return undefined if the color already meets the contrast ratio (black bg)', () => {
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 1), undefined);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 2), undefined);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 3), undefined);
});
it('should return a color that meets the contrast ratio (black bg)', () => {
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 4), 0x707070ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 5), 0x7f7f7fff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 6), 0x8c8c8cff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 7), 0x989898ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 8), 0xa3a3a3ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 9), 0xadadadff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 10), 0xb6b6b6ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 11), 0xbebebeff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 12), 0xc5c5c5ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 13), 0xd1d1d1ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 14), 0xd6d6d6ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 15), 0xdbdbdbff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 16), 0xe3e3e3ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 17), 0xe9e9e9ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 18), 0xeeeeeeff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 19), 0xf4f4f4ff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 20), 0xfafafaff);
assert.equal(ensureContrastRatioRgba(0x000000ff, 0x606060ff, 21), 0xffffffff);
});
it('should return undefined if the color already meets the contrast ratio (white bg)', () => {
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 1), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 2), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 3), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 4), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 5), undefined);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 6), undefined);
});
it('should return a color that meets the contrast ratio (white bg)', () => {
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 7), 0x565656ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 8), 0x4d4d4dff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 9), 0x454545ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 10), 0x3e3e3eff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 11), 0x373737ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 12), 0x313131ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 13), 0x313131ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 14), 0x272727ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 15), 0x232323ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 16), 0x1f1f1fff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 17), 0x1b1b1bff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 18), 0x151515ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 19), 0x101010ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 20), 0x080808ff);
assert.equal(ensureContrastRatioRgba(0xffffffff, 0x606060ff, 21), 0x000000ff);
});
});
});
+181
View File
@@ -0,0 +1,181 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor } from 'browser/Types';
export function blend(bg: IColor, fg: IColor): IColor {
const a = (fg.rgba & 0xFF) / 255;
if (a === 1) {
return {
css: fg.css,
rgba: fg.rgba
};
}
const fgR = (fg.rgba >> 24) & 0xFF;
const fgG = (fg.rgba >> 16) & 0xFF;
const fgB = (fg.rgba >> 8) & 0xFF;
const bgR = (bg.rgba >> 24) & 0xFF;
const bgG = (bg.rgba >> 16) & 0xFF;
const bgB = (bg.rgba >> 8) & 0xFF;
const r = bgR + Math.round((fgR - bgR) * a);
const g = bgG + Math.round((fgG - bgG) * a);
const b = bgB + Math.round((fgB - bgB) * a);
const css = toCss(r, g, b);
const rgba = toRgba(r, g, b);
return { css, rgba };
}
export function fromCss(css: string): IColor {
return {
css,
rgba: (parseInt(css.slice(1), 16) << 8 | 0xFF) >>> 0
};
}
export function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
export function toCss(r: number, g: number, b: number, a?: number): string {
if (a !== undefined) {
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}${toPaddedHex(a)}`;
}
return `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`;
}
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
// >>> 0 forces an unsigned int
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
}
export function fromRgba(value: number): [number, number, number, number] {
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
}
export function opaque(color: IColor): IColor {
const rgba = (color.rgba | 0xFF) >>> 0;
const [r, g, b] = fromRgba(rgba);
return {
css: toCss(r, g, b),
rgba
};
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param rgb The color to use.
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function rgbRelativeLuminance(rgb: number): number {
return rgbRelativeLuminance2(
(rgb >> 16) & 0xFF,
(rgb >> 8 ) & 0xFF,
(rgb ) & 0xFF);
}
/**
* Gets the relative luminance of an RGB color, this is useful in determining the contrast ratio
* between two colors.
* @param r The red channel (0x00 to 0xFF).
* @param g The green channel (0x00 to 0xFF).
* @param b The blue channel (0x00 to 0xFF).
* @see https://www.w3.org/TR/WCAG20/#relativeluminancedef
*/
export function rgbRelativeLuminance2(r: number, g: number, b: number): number {
const rs = r / 255;
const gs = g / 255;
const bs = b / 255;
const rr = rs <= 0.03928 ? rs / 12.92 : Math.pow((rs + 0.055) / 1.055, 2.4);
const rg = gs <= 0.03928 ? gs / 12.92 : Math.pow((gs + 0.055) / 1.055, 2.4);
const rb = bs <= 0.03928 ? bs / 12.92 : Math.pow((bs + 0.055) / 1.055, 2.4);
return rr * 0.2126 + rg * 0.7152 + rb * 0.0722;
}
/**
* Gets the contrast ratio between two relative luminance values.
* @param l1 The first relative luminance.
* @param l2 The first relative luminance.
* @see https://www.w3.org/TR/WCAG20/#contrast-ratiodef
*/
export function contrastRatio(l1: number, l2: number): number {
if (l1 < l2) {
return (l2 + 0.05) / (l1 + 0.05);
}
return (l1 + 0.05) / (l2 + 0.05);
}
export function rgbaToColor(r: number, g: number, b: number): IColor {
return {
css: toCss(r, g, b),
rgba: toRgba(r, g, b)
};
}
export function ensureContrastRatioRgba(bgRgba: number, fgRgba: number, ratio: number): number | undefined {
const bgL = rgbRelativeLuminance(bgRgba >> 8);
const fgL = rgbRelativeLuminance(fgRgba >> 8);
const cr = contrastRatio(bgL, fgL);
if (cr < ratio) {
if (fgL < bgL) {
return reduceLuminance(bgRgba, fgRgba, ratio);
}
return increaseLuminance(bgRgba, fgRgba, ratio);
}
return undefined;
}
export function ensureContrastRatio(bg: IColor, fg: IColor, ratio: number): IColor | undefined {
const result = ensureContrastRatioRgba(bg.rgba, fg.rgba, ratio);
if (!result) {
return undefined;
}
return rgbaToColor(
(result >> 24 & 0xFF),
(result >> 16 & 0xFF),
(result >> 8 & 0xFF)
);
}
export function reduceLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to reducing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR > 0 || fgG > 0 || fgB > 0)) {
// Reduce by 10% until the ratio is hit
fgR -= Math.max(0, Math.ceil(fgR * 0.1));
fgG -= Math.max(0, Math.ceil(fgG * 0.1));
fgB -= Math.max(0, Math.ceil(fgB * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
export function increaseLuminance(bgRgba: number, fgRgba: number, ratio: number): number {
// This is a naive but fast approach to increasing luminance as converting to
// HSL and back is expensive
const bgR = (bgRgba >> 24) & 0xFF;
const bgG = (bgRgba >> 16) & 0xFF;
const bgB = (bgRgba >> 8) & 0xFF;
let fgR = (fgRgba >> 24) & 0xFF;
let fgG = (fgRgba >> 16) & 0xFF;
let fgB = (fgRgba >> 8) & 0xFF;
let cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
while (cr < ratio && (fgR < 0xFF || fgG < 0xFF || fgB < 0xFF)) {
// Increase by 10% until the ratio is hit
fgR = Math.min(0xFF, fgR + Math.ceil((255 - fgR) * 0.1));
fgG = Math.min(0xFF, fgG + Math.ceil((255 - fgG) * 0.1));
fgB = Math.min(0xFF, fgB + Math.ceil((255 - fgB) * 0.1));
cr = contrastRatio(rgbRelativeLuminance2(fgR, fgB, fgG), rgbRelativeLuminance2(bgR, bgG, bgB));
}
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
}
+43
View File
@@ -0,0 +1,43 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { ColorContrastCache } from 'browser/ColorContrastCache';
describe('ColorContrastCache', () => {
let cache: ColorContrastCache;
beforeEach(() => {
cache = new ColorContrastCache();
});
it('should save and get color values', () => {
assert.strictEqual(cache.getColor(0x01, 0x00), undefined);
cache.setColor(0x01, 0x01, null);
assert.strictEqual(cache.getColor(0x01, 0x01), null);
cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff});
assert.deepEqual(cache.getColor(0x01, 0x02), { css: '#030303', rgba: 0x030303ff});
});
it('should save and get css values', () => {
assert.strictEqual(cache.getCss(0x01, 0x00), undefined);
cache.setCss(0x01, 0x01, null);
assert.strictEqual(cache.getCss(0x01, 0x01), null);
cache.setCss(0x01, 0x02, '#030303');
assert.deepEqual(cache.getCss(0x01, 0x02), '#030303');
});
it('should clear all values on clear', () => {
cache.setColor(0x01, 0x01, null);
cache.setColor(0x01, 0x02, { css: '#030303', rgba: 0x030303ff});
cache.setCss(0x01, 0x01, null);
cache.setCss(0x01, 0x02, '#030303');
cache.clear();
assert.strictEqual(cache.getColor(0x01, 0x01), undefined);
assert.strictEqual(cache.getColor(0x01, 0x02), undefined);
assert.strictEqual(cache.getCss(0x01, 0x01), undefined);
assert.strictEqual(cache.getCss(0x01, 0x02), undefined);
});
});
+38
View File
@@ -0,0 +1,38 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IColor, IColorContrastCache } from 'browser/Types';
export class ColorContrastCache implements IColorContrastCache {
private _color: { [bg: number]: { [fg: number]: IColor | null | undefined } | undefined } = {};
private _rgba: { [bg: number]: { [fg: number]: string | null | undefined } | undefined } = {};
public clear(): void {
this._color = {};
this._rgba = {};
}
public setCss(bg: number, fg: number, value: string | null): void {
if (!this._rgba[bg]) {
this._rgba[bg] = {};
}
this._rgba[bg]![fg] = value;
}
public getCss(bg: number, fg: number): string | null | undefined {
return this._rgba[bg] ? this._rgba[bg]![fg] : undefined;
}
public setColor(bg: number, fg: number, value: IColor | null): void {
if (!this._color[bg]) {
this._color[bg] = {};
}
this._color[bg]![fg] = value;
}
public getColor(bg: number, fg: number): IColor | null | undefined {
return this._color[bg] ? this._color[bg]![fg] : undefined;
}
}
+1 -1
View File
@@ -34,7 +34,7 @@ describe('ColorManager', () => {
describe('constructor', () => { describe('constructor', () => {
it('should fill all colors with values', () => { it('should fill all colors with values', () => {
for (const key of Object.keys(cm.colors)) { for (const key of Object.keys(cm.colors)) {
if (key !== 'ansi') { if (key !== 'ansi' && key !== 'contrastCache') {
// A #rrggbb or rgba(...) // A #rrggbb or rgba(...)
assert.ok((<any>cm.colors)[key].css.length >= 7); assert.ok((<any>cm.colors)[key].css.length >= 7);
} }
+92 -64
View File
@@ -3,16 +3,18 @@
* @license MIT * @license MIT
*/ */
import { IColorManager, IColor, IColorSet } from 'browser/Types'; import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/Types';
import { ITheme } from 'common/services/Services'; import { ITheme } from 'common/services/Services';
import { fromCss, toCss, blend, toRgba, toPaddedHex } from 'browser/Color';
import { ColorContrastCache } from 'browser/ColorContrastCache';
const DEFAULT_FOREGROUND = fromHex('#ffffff'); const DEFAULT_FOREGROUND = fromCss('#ffffff');
const DEFAULT_BACKGROUND = fromHex('#000000'); const DEFAULT_BACKGROUND = fromCss('#000000');
const DEFAULT_CURSOR = fromHex('#ffffff'); const DEFAULT_CURSOR = fromCss('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromHex('#000000'); const DEFAULT_CURSOR_ACCENT = fromCss('#000000');
const DEFAULT_SELECTION = { const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)', css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF77 rgba: 0xFFFFFF4D
}; };
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy // An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
@@ -20,23 +22,23 @@ const DEFAULT_SELECTION = {
export const DEFAULT_ANSI_COLORS = (() => { export const DEFAULT_ANSI_COLORS = (() => {
const colors = [ const colors = [
// dark: // dark:
fromHex('#2e3436'), fromCss('#2e3436'),
fromHex('#cc0000'), fromCss('#cc0000'),
fromHex('#4e9a06'), fromCss('#4e9a06'),
fromHex('#c4a000'), fromCss('#c4a000'),
fromHex('#3465a4'), fromCss('#3465a4'),
fromHex('#75507b'), fromCss('#75507b'),
fromHex('#06989a'), fromCss('#06989a'),
fromHex('#d3d7cf'), fromCss('#d3d7cf'),
// bright: // bright:
fromHex('#555753'), fromCss('#555753'),
fromHex('#ef2929'), fromCss('#ef2929'),
fromHex('#8ae234'), fromCss('#8ae234'),
fromHex('#fce94f'), fromCss('#fce94f'),
fromHex('#729fcf'), fromCss('#729fcf'),
fromHex('#ad7fa8'), fromCss('#ad7fa8'),
fromHex('#34e2e2'), fromCss('#34e2e2'),
fromHex('#eeeeec') fromCss('#eeeeec')
]; ];
// Fill in the remaining 240 ANSI colors. // Fill in the remaining 240 ANSI colors.
@@ -47,37 +49,23 @@ export const DEFAULT_ANSI_COLORS = (() => {
const g = v[(i / 6) % 6 | 0]; const g = v[(i / 6) % 6 | 0];
const b = v[i % 6]; const b = v[i % 6];
colors.push({ colors.push({
css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`, css: toCss(r, g, b),
// Use >>> 0 to force a conversion to an unsigned int rgba: toRgba(r, g, b)
rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
}); });
} }
// Generate greys (232-255) // Generate greys (232-255)
for (let i = 0; i < 24; i++) { for (let i = 0; i < 24; i++) {
const c = 8 + i * 10; const c = 8 + i * 10;
const ch = toPaddedHex(c);
colors.push({ colors.push({
css: `#${ch}${ch}${ch}`, css: toCss(c, c, c),
rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0 rgba: toRgba(c, c, c)
}); });
} }
return colors; return colors;
})(); })();
function fromHex(css: string): IColor {
return {
css,
rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
};
}
function toPaddedHex(c: number): string {
const s = c.toString(16);
return s.length < 2 ? '0' + s : s;
}
/** /**
* Manages the source of truth for a terminal's colors. * Manages the source of truth for a terminal's colors.
*/ */
@@ -85,6 +73,7 @@ export class ColorManager implements IColorManager {
public colors: IColorSet; public colors: IColorSet;
private _ctx: CanvasRenderingContext2D; private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient; private _litmusColor: CanvasGradient;
private _contrastCache: IColorContrastCache;
constructor(document: Document, public allowTransparency: boolean) { constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@@ -97,16 +86,25 @@ export class ColorManager implements IColorManager {
this._ctx = ctx; this._ctx = ctx;
this._ctx.globalCompositeOperation = 'copy'; this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1); this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
this._contrastCache = new ColorContrastCache();
this.colors = { this.colors = {
foreground: DEFAULT_FOREGROUND, foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND, background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR, cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT, cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION, selection: DEFAULT_SELECTION,
ansi: DEFAULT_ANSI_COLORS.slice() selectionOpaque: blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
ansi: DEFAULT_ANSI_COLORS.slice(),
contrastCache: this._contrastCache
}; };
} }
public onOptionsChange(key: string): void {
if (key === 'minimumContrastRatio') {
this._contrastCache.clear();
}
}
/** /**
* Sets the terminal's theme. * Sets the terminal's theme.
* @param theme The theme to use. If a partial theme is provided then default * @param theme The theme to use. If a partial theme is provided then default
@@ -118,6 +116,7 @@ export class ColorManager implements IColorManager {
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true); this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true); this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true); this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.selectionOpaque = blend(this.colors.background, this.colors.selection);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]); 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[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]); this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
@@ -134,6 +133,8 @@ export class ColorManager implements IColorManager {
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]); 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[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]); this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
// Clear our the cache
this._contrastCache.clear();
} }
private _parseColor( private _parseColor(
@@ -158,33 +159,60 @@ export class ColorManager implements IColorManager {
this._ctx.fillRect(0, 0, 1, 1); this._ctx.fillRect(0, 0, 1, 1);
const data = this._ctx.getImageData(0, 0, 1, 1).data; const data = this._ctx.getImageData(0, 0, 1, 1).data;
if (!allowTransparency && data[3] !== 0xFF) { // Check if the printed color was transparent
// Ideally we'd just ignore the alpha channel, but... if (data[3] !== 0xFF) {
// if (!allowTransparency) {
// Browsers may not give back exactly the same RGB values we put in, because most/all // Ideally we'd just ignore the alpha channel, but...
// 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 // Browsers may not give back exactly the same RGB values we put in, because most/all
// on their own. // 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
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns // on their own.
// into 0x00000000. //
// // E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values, // into 0x00000000.
// pixels that have just been set using putImageData() might be returned to an equivalent //
// getImageData() as different values." // "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation // pixels that have just been set using putImageData() might be returned to an equivalent
// // getImageData() as different values."
// So let's just use the fallback color in this case instead. // -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
console.warn( //
`Color: ${css} is using transparency, but allowTransparency is false. ` + // So let's just use the fallback color in this case instead.
`Using fallback ${fallback.css}.` console.warn(
); `Color: ${css} is using transparency, but allowTransparency is false. ` +
return fallback; `Using fallback ${fallback.css}.`
);
return fallback;
}
let r: number;
let g: number;
let b: number;
let a: number;
let rgba: number;
if (css.length === 5) {
const num = parseInt(css.substr(1), 16);
r = ((num >> 12) & 0xF) * 16;
g = ((num >> 8) & 0xF) * 16;
b = ((num >> 4) & 0xF) * 16;
a = (num & 0xF) * 16;
rgba = toRgba(r, g, b, a);
} else {
rgba = parseInt(css.substr(1), 16);
r = (rgba >> 24) & 0xFF;
g = (rgba >> 16) & 0xFF;
b = (rgba >> 8) & 0xFF;
a = (rgba ) & 0xFF;
}
return {
rgba,
css: toCss(r, g, b, a)
};
} }
return { return {
css, css,
rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0 rgba: toRgba(data[0], data[1], data[2], data[3])
}; };
} }
} }
+4 -2
View File
@@ -211,7 +211,7 @@ export class Linkifier implements ILinkifier {
*/ */
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void { private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
// clone regex to do a global search on text // clone regex to do a global search on text
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g'); const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g');
let match; let match;
let stringIndex = -1; let stringIndex = -1;
while ((match = rex.exec(text)) !== null) { while ((match = rex.exec(text)) !== null) {
@@ -306,7 +306,9 @@ export class Linkifier implements ILinkifier {
e => { e => {
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
if (matcher.hoverTooltipCallback) { if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri); // Note that IViewportRange use 1-based coordinates to align with escape sequences such
// as CUP which use 1,1 as the default for row/col
matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } });
} }
}, },
() => { () => {
+3 -5
View File
@@ -175,11 +175,9 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
// Find the active zone, prevent event propagation if found to prevent other // Find the active zone, prevent event propagation if found to prevent other
// components from handling the mouse event. // components from handling the mouse event.
const zone = this._findZoneEventAt(e); const zone = this._findZoneEventAt(e);
if (zone) { if (zone?.willLinkActivate(e)) {
if (zone.willLinkActivate(e)) { e.preventDefault();
e.preventDefault(); e.stopImmediatePropagation();
e.stopImmediatePropagation();
}
} }
} }
+26 -3
View File
@@ -8,6 +8,7 @@ import { IDisposable } from 'common/Types';
export interface IColorManager { export interface IColorManager {
colors: IColorSet; colors: IColorSet;
onOptionsChange(key: string): void;
} }
export interface IColor { export interface IColor {
@@ -21,7 +22,18 @@ export interface IColorSet {
cursor: IColor; cursor: IColor;
cursorAccent: IColor; cursorAccent: IColor;
selection: IColor; selection: IColor;
/** The selection blended on top of background. */
selectionOpaque: IColor;
ansi: IColor[]; ansi: IColor[];
contrastCache: IColorContrastCache;
}
export interface IColorContrastCache {
clear(): void;
setCss(bg: number, fg: number, value: string | null): void;
getCss(bg: number, fg: number): string | null | undefined;
setColor(bg: number, fg: number, value: IColor | null): void;
getColor(bg: number, fg: number): IColor | null | undefined;
} }
export interface IPartialColorSet { export interface IPartialColorSet {
@@ -35,7 +47,7 @@ export interface IPartialColorSet {
export interface IViewport extends IDisposable { export interface IViewport extends IDisposable {
scrollBarWidth: number; scrollBarWidth: number;
syncScrollArea(): void; syncScrollArea(immediate?: boolean): void;
getLinesScrolled(ev: WheelEvent): number; getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): boolean; onWheel(ev: WheelEvent): boolean;
onTouchStart(ev: TouchEvent): void; onTouchStart(ev: TouchEvent): void;
@@ -43,14 +55,25 @@ export interface IViewport extends IDisposable {
onThemeChange(colors: IColorSet): void; onThemeChange(colors: IColorSet): void;
} }
export interface IViewportRange {
start: IViewportRangePosition;
end: IViewportRangePosition;
}
export interface IViewportRangePosition {
x: number;
y: number;
}
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export interface ILinkMatcher { export interface ILinkMatcher {
id: number; id: number;
regex: RegExp; regex: RegExp;
handler: LinkMatcherHandler; handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler; hoverTooltipCallback?: LinkMatcherHoverTooltipCallback;
hoverLeaveCallback?: () => void; hoverLeaveCallback?: () => void;
matchIndex?: number; matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback; validationCallback?: LinkMatcherValidationCallback;
@@ -96,7 +119,7 @@ export interface ILinkMatcherOptions {
/** /**
* A callback that fires when the mouse hovers over a link. * A callback that fires when the mouse hovers over a link.
*/ */
tooltipCallback?: LinkMatcherHandler; tooltipCallback?: LinkMatcherHoverTooltipCallback;
/** /**
* A callback that fires when the mouse leaves a link that was hovered. * A callback that fires when the mouse leaves a link that was hovered.
*/ */
+30 -11
View File
@@ -7,7 +7,7 @@ import { Disposable } from 'common/Lifecycle';
import { addDisposableDomListener } from 'browser/Lifecycle'; import { addDisposableDomListener } from 'browser/Lifecycle';
import { IColorSet, IViewport } from 'browser/Types'; import { IColorSet, IViewport } from 'browser/Types';
import { ICharSizeService, IRenderService } from 'browser/services/Services'; import { ICharSizeService, IRenderService } from 'browser/services/Services';
import { IBufferService } from 'common/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services';
const FALLBACK_SCROLL_BAR_WIDTH = 15; const FALLBACK_SCROLL_BAR_WIDTH = 15;
@@ -37,6 +37,7 @@ export class Viewport extends Disposable implements IViewport {
private readonly _viewportElement: HTMLElement, private readonly _viewportElement: HTMLElement,
private readonly _scrollArea: HTMLElement, private readonly _scrollArea: HTMLElement,
@IBufferService private readonly _bufferService: IBufferService, @IBufferService private readonly _bufferService: IBufferService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICharSizeService private readonly _charSizeService: ICharSizeService, @ICharSizeService private readonly _charSizeService: ICharSizeService,
@IRenderService private readonly _renderService: IRenderService @IRenderService private readonly _renderService: IRenderService
) { ) {
@@ -60,7 +61,14 @@ export class Viewport extends Disposable implements IViewport {
* Refreshes row height, setting line-height, viewport height and scroll area height if * Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary. * necessary.
*/ */
private _refresh(): void { private _refresh(immediate: boolean): void {
if (immediate) {
this._innerRefresh();
if (this._refreshAnimationFrame !== null) {
cancelAnimationFrame(this._refreshAnimationFrame);
}
return;
}
if (this._refreshAnimationFrame === null) { if (this._refreshAnimationFrame === null) {
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh()); this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
} }
@@ -88,40 +96,39 @@ export class Viewport extends Disposable implements IViewport {
this._refreshAnimationFrame = null; this._refreshAnimationFrame = null;
} }
/** /**
* Updates dimensions and synchronizes the scroll area if necessary. * Updates dimensions and synchronizes the scroll area if necessary.
*/ */
public syncScrollArea(): void { public syncScrollArea(immediate: boolean = false): void {
// If buffer height changed // If buffer height changed
if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) { if (this._lastRecordedBufferLength !== this._bufferService.buffer.lines.length) {
this._lastRecordedBufferLength = this._bufferService.buffer.lines.length; this._lastRecordedBufferLength = this._bufferService.buffer.lines.length;
this._refresh(); this._refresh(immediate);
return; return;
} }
// If viewport height changed // If viewport height changed
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) { if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
this._refresh(); this._refresh(immediate);
return; return;
} }
// If the buffer position doesn't match last scroll top // If the buffer position doesn't match last scroll top
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight; const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
if (this._lastScrollTop !== newScrollTop) { if (this._lastScrollTop !== newScrollTop) {
this._refresh(); this._refresh(immediate);
return; return;
} }
// If element's scroll top changed, this can happen when hiding the element // If element's scroll top changed, this can happen when hiding the element
if (this._lastScrollTop !== this._viewportElement.scrollTop) { if (this._lastScrollTop !== this._viewportElement.scrollTop) {
this._refresh(); this._refresh(immediate);
return; return;
} }
// If row height changed // If row height changed
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) { if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh(); this._refresh(immediate);
return; return;
} }
} }
@@ -191,7 +198,7 @@ export class Viewport extends Disposable implements IViewport {
} }
// Fallback to WheelEvent.DOM_DELTA_PIXEL // Fallback to WheelEvent.DOM_DELTA_PIXEL
let amount = ev.deltaY; let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) { if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight; amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) { } else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
@@ -212,7 +219,7 @@ export class Viewport extends Disposable implements IViewport {
} }
// Fallback to WheelEvent.DOM_DELTA_LINE // Fallback to WheelEvent.DOM_DELTA_LINE
let amount = ev.deltaY; let amount = this._applyScrollModifier(ev.deltaY, ev);
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) { if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount; this._wheelPartialScroll += amount;
@@ -224,6 +231,18 @@ export class Viewport extends Disposable implements IViewport {
return amount; return amount;
} }
private _applyScrollModifier(amount: number, ev: WheelEvent): number {
const modifier = this._optionsService.options.fastScrollModifier;
// Multiply the scroll speed when the modifier is down
if ((modifier === 'alt' && ev.altKey) ||
(modifier === 'ctrl' && ev.ctrlKey) ||
(modifier === 'shift' && ev.shiftKey)) {
return amount * this._optionsService.options.fastScrollSensitivity * this._optionsService.options.scrollSensitivity;
}
return amount * this._optionsService.options.scrollSensitivity;
}
/** /**
* Handles the touchstart event, recording the touch occurred. * Handles the touchstart event, recording the touch occurred.
* @param ev The touch event. * @param ev The touch event.
+104 -13
View File
@@ -5,16 +5,17 @@
import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
import { ICellData } from 'common/Types'; import { ICellData } from 'common/Types';
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
import { AttributeData } from 'common/buffer/AttributeData'; import { AttributeData } from 'common/buffer/AttributeData';
import { IColorSet } from 'browser/Types'; import { IColorSet, IColor } from 'browser/Types';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBufferService, IOptionsService } from 'common/services/Services';
import { throwIfFalsy } from 'browser/renderer/RendererUtils'; import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { toCss, ensureContrastRatioRgba, opaque } from 'browser/Color';
export abstract class BaseRenderLayer implements IRenderLayer { export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
@@ -60,9 +61,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
public dispose(): void { public dispose(): void {
this._container.removeChild(this._canvas); this._container.removeChild(this._canvas);
if (this._charAtlas) { this._charAtlas?.dispose();
this._charAtlas.dispose();
}
} }
private _initCanvas(): void { private _initCanvas(): void {
@@ -264,13 +263,14 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param bold Whether the text is bold. * @param bold Whether the text is bold.
*/ */
protected _drawChars(cell: ICellData, x: number, y: number): void { protected _drawChars(cell: ICellData, x: number, y: number): void {
const contrastColor = this._getContrastColor(cell);
// skip cache right away if we draw in RGB // skip cache right away if we draw in RGB
// Note: to avoid bad runtime JoinedCellData will be skipped // Note: to avoid bad runtime JoinedCellData will be skipped
// in the cache handler itself (atlasDidDraw == false) and // in the cache handler itself (atlasDidDraw == false) and
// fall through to uncached later down below // fall through to uncached later down below
if (cell.isFgRGB() || cell.isBgRGB()) { if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
this._drawUncachedChars(cell, x, y); this._drawUncachedChars(cell, x, y, contrastColor);
return; return;
} }
@@ -284,7 +284,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor(); fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
} }
const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8 && fg !== INVERTED_DEFAULT_COLOR; const drawInBrightColor = this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
fg += drawInBrightColor ? 8 : 0; fg += drawInBrightColor ? 8 : 0;
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR; this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
@@ -316,21 +316,29 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at. * @param x The column to draw at.
* @param y The row to draw at. * @param y The row to draw at.
*/ */
private _drawUncachedChars(cell: ICellData, x: number, y: number): void { private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
this._ctx.save(); this._ctx.save();
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
this._ctx.textBaseline = 'middle'; this._ctx.textBaseline = 'middle';
if (cell.isInverse()) { if (cell.isInverse()) {
if (cell.isBgDefault()) { if (fgOverride) {
this._ctx.fillStyle = this._colors.background.css; this._ctx.fillStyle = fgOverride.css;
} else if (cell.isBgDefault()) {
this._ctx.fillStyle = opaque(this._colors.background).css;
} else if (cell.isBgRGB()) { } else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else { } else {
this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
} }
} else { } else {
if (cell.isFgDefault()) { if (fgOverride) {
this._ctx.fillStyle = fgOverride.css;
} else if (cell.isFgDefault()) {
this._ctx.fillStyle = this._colors.foreground.css; this._ctx.fillStyle = this._colors.foreground.css;
} else if (cell.isFgRGB()) { } else if (cell.isFgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`; this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
@@ -381,5 +389,88 @@ export abstract class BaseRenderLayer implements IRenderLayer {
return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`; return `${fontStyle} ${fontWeight} ${this._optionsService.options.fontSize * window.devicePixelRatio}px ${this._optionsService.options.fontFamily}`;
} }
private _getContrastColor(cell: CellData): IColor | undefined {
if (this._optionsService.options.minimumContrastRatio === 1) {
return undefined;
}
// Try get from cache first
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
if (adjustedColor !== undefined) {
return adjustedColor || undefined;
}
let fgColor = cell.getFgColor();
let fgColorMode = cell.getFgColorMode();
let bgColor = cell.getBgColor();
let bgColorMode = cell.getBgColorMode();
const isInverse = !!cell.isInverse();
const isBold = !!cell.isInverse();
if (isInverse) {
const temp = fgColor;
fgColor = bgColor;
bgColor = temp;
const temp2 = fgColorMode;
fgColorMode = bgColorMode;
bgColorMode = temp2;
}
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, isInverse);
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._optionsService.options.minimumContrastRatio);
if (!result) {
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
return undefined;
}
const color: IColor = {
css: toCss(
(result >> 24) & 0xFF,
(result >> 16) & 0xFF,
(result >> 8) & 0xFF
),
rgba: result
};
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
return color;
}
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
return this._colors.ansi[bgColor].rgba;
case Attributes.CM_RGB:
return bgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.foreground.rgba;
}
return this._colors.background.rgba;
}
}
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
switch (fgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
if (this._optionsService.options.drawBoldTextInBrightColors && bold && fgColor < 8) {
fgColor += 8;
}
return this._colors.ansi[fgColor].rgba;
case Attributes.CM_RGB:
return fgColor << 8;
case Attributes.CM_DEFAULT:
default:
if (inverse) {
return this._colors.background.rgba;
}
return this._colors.foreground.rgba;
}
}
} }
@@ -303,7 +303,6 @@ export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
// current range // current range
ranges[i - 1][1] = Math.max(newRange[1], range[1]); ranges[i - 1][1] = Math.max(newRange[1], range[1]);
ranges.splice(i, 1); ranges.splice(i, 1);
inRange = false;
return ranges; return ranges;
} }
@@ -3,13 +3,14 @@
* @license MIT * @license MIT
*/ */
import { IRenderDimensions } from 'browser/renderer/Types'; import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { BaseRenderLayer } from '../browser/renderer/BaseRenderLayer'; import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
import { ITerminal } from '../Types';
import { ICellData } from 'common/Types'; import { ICellData } from 'common/Types';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { IEventEmitter } from 'common/EventEmitter';
import { ICoreBrowserService } from 'browser/services/Services';
interface ICursorState { interface ICursorState {
x: number; x: number;
@@ -27,25 +28,27 @@ const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer { export class CursorRenderLayer extends BaseRenderLayer {
private _state: ICursorState; private _state: ICursorState;
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void}; private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager; private _cursorBlinkStateManager: CursorBlinkStateManager | undefined;
private _cell: ICellData = new CellData(); private _cell: ICellData = new CellData();
constructor( constructor(
container: HTMLElement, container: HTMLElement,
zIndex: number, zIndex: number,
colors: IColorSet, colors: IColorSet,
private _terminal: ITerminal,
rendererId: number, rendererId: number,
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRefreshRowsEvent>,
readonly bufferService: IBufferService, readonly bufferService: IBufferService,
readonly optionsService: IOptionsService readonly optionsService: IOptionsService,
private readonly _coreService: ICoreService,
private readonly _coreBrowserService: ICoreBrowserService
) { ) {
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService); super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
this._state = { this._state = {
x: null, x: 0,
y: null, y: 0,
isFocused: null, isFocused: false,
style: null, style: '',
width: null width: 0
}; };
this._cursorRenderers = { this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this), 'bar': this._renderBarCursor.bind(this),
@@ -59,11 +62,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
super.resize(dim); super.resize(dim);
// Resizing the canvas discards the contents of the canvas so clear state // Resizing the canvas discards the contents of the canvas so clear state
this._state = { this._state = {
x: null, x: 0,
y: null, y: 0,
isFocused: null, isFocused: false,
style: null, style: '',
width: null width: 0
}; };
} }
@@ -71,7 +74,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
this._clearCursor(); this._clearCursor();
if (this._cursorBlinkStateManager) { if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.dispose(); this._cursorBlinkStateManager.dispose();
this._cursorBlinkStateManager = null; this._cursorBlinkStateManager = undefined;
this.onOptionsChanged(); this.onOptionsChanged();
} }
} }
@@ -80,33 +83,31 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._cursorBlinkStateManager) { if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.pause(); this._cursorBlinkStateManager.pause();
} }
this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
} }
public onFocus(): void { public onFocus(): void {
if (this._cursorBlinkStateManager) { if (this._cursorBlinkStateManager) {
this._cursorBlinkStateManager.resume(); this._cursorBlinkStateManager.resume();
} else { } else {
this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y); this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
} }
} }
public onOptionsChanged(): void { public onOptionsChanged(): void {
if (this._optionsService.options.cursorBlink) { if (this._optionsService.options.cursorBlink) {
if (!this._cursorBlinkStateManager) { if (!this._cursorBlinkStateManager) {
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._terminal.isFocused, () => { this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
this._render(true); this._render(true);
}); });
} }
} else { } else {
if (this._cursorBlinkStateManager) { this._cursorBlinkStateManager?.dispose();
this._cursorBlinkStateManager.dispose(); this._cursorBlinkStateManager = undefined;
this._cursorBlinkStateManager = null;
}
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._terminal.refresh(this._bufferService.buffer.y, this._bufferService.buffer.y);
} }
// Request a refresh from the terminal as management of rendering is being
// moved back to the terminal
this._onRequestRefreshRowsEvent.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
} }
public onCursorMove(): void { public onCursorMove(): void {
@@ -125,7 +126,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
private _render(triggeredByAnimationFrame: boolean): void { private _render(triggeredByAnimationFrame: boolean): void {
// Don't draw the cursor if it's hidden // Don't draw the cursor if it's hidden
if (!this._terminal.cursorState || this._terminal.cursorHidden) { if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
this._clearCursor(); this._clearCursor();
return; return;
} }
@@ -139,21 +140,26 @@ export class CursorRenderLayer extends BaseRenderLayer {
return; return;
} }
this._bufferService.buffer.lines.get(cursorY).loadCell(this._bufferService.buffer.x, this._cell); this._bufferService.buffer.lines.get(cursorY)!.loadCell(this._bufferService.buffer.x, this._cell);
if (this._cell.content === undefined) { if (this._cell.content === undefined) {
return; return;
} }
if (!this._terminal.isFocused) { if (!this._coreBrowserService.isFocused) {
this._clearCursor(); this._clearCursor();
this._ctx.save(); this._ctx.save();
this._ctx.fillStyle = this._colors.cursor.css; this._ctx.fillStyle = this._colors.cursor.css;
this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell); const cursorStyle = this._optionsService.options.cursorStyle;
if (cursorStyle && cursorStyle !== 'block') {
this._cursorRenderers[cursorStyle](this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
} else {
this._renderBlurCursor(this._bufferService.buffer.x, viewportRelativeCursorY, this._cell);
}
this._ctx.restore(); this._ctx.restore();
this._state.x = this._bufferService.buffer.x; this._state.x = this._bufferService.buffer.x;
this._state.y = viewportRelativeCursorY; this._state.y = viewportRelativeCursorY;
this._state.isFocused = false; this._state.isFocused = false;
this._state.style = this._optionsService.options.cursorStyle; this._state.style = cursorStyle;
this._state.width = this._cell.getWidth(); this._state.width = this._cell.getWidth();
return; return;
} }
@@ -168,7 +174,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
// The cursor is already in the correct spot, don't redraw // The cursor is already in the correct spot, don't redraw
if (this._state.x === this._bufferService.buffer.x && if (this._state.x === this._bufferService.buffer.x &&
this._state.y === viewportRelativeCursorY && this._state.y === viewportRelativeCursorY &&
this._state.isFocused === this._terminal.isFocused && this._state.isFocused === this._coreBrowserService.isFocused &&
this._state.style === this._optionsService.options.cursorStyle && this._state.style === this._optionsService.options.cursorStyle &&
this._state.width === this._cell.getWidth()) { this._state.width === this._cell.getWidth()) {
return; return;
@@ -191,11 +197,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (this._state) { if (this._state) {
this._clearCells(this._state.x, this._state.y, this._state.width, 1); this._clearCells(this._state.x, this._state.y, this._state.width, 1);
this._state = { this._state = {
x: null, x: 0,
y: null, y: 0,
isFocused: null, isFocused: false,
style: null, style: '',
width: null width: 0
}; };
} }
} }
@@ -234,16 +240,16 @@ export class CursorRenderLayer extends BaseRenderLayer {
class CursorBlinkStateManager { class CursorBlinkStateManager {
public isCursorVisible: boolean; public isCursorVisible: boolean;
private _animationFrame: number; private _animationFrame: number | undefined;
private _blinkStartTimeout: number; private _blinkStartTimeout: number | undefined;
private _blinkInterval: number; private _blinkInterval: number | undefined;
/** /**
* The time at which the animation frame was restarted, this is used on the * The time at which the animation frame was restarted, this is used on the
* next render to restart the timers so they don't need to restart the timers * next render to restart the timers so they don't need to restart the timers
* multiple times over a short period. * multiple times over a short period.
*/ */
private _animationTimeRestarted: number; private _animationTimeRestarted: number | undefined;
constructor( constructor(
isFocused: boolean, isFocused: boolean,
@@ -260,15 +266,15 @@ class CursorBlinkStateManager {
public dispose(): void { public dispose(): void {
if (this._blinkInterval) { if (this._blinkInterval) {
window.clearInterval(this._blinkInterval); window.clearInterval(this._blinkInterval);
this._blinkInterval = null; this._blinkInterval = undefined;
} }
if (this._blinkStartTimeout) { if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout); window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = null; this._blinkStartTimeout = undefined;
} }
if (this._animationFrame) { if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame); window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null; this._animationFrame = undefined;
} }
} }
@@ -283,7 +289,7 @@ class CursorBlinkStateManager {
if (!this._animationFrame) { if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => { this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback(); this._renderCallback();
this._animationFrame = null; this._animationFrame = undefined;
}); });
} }
} }
@@ -303,7 +309,7 @@ class CursorBlinkStateManager {
// started // started
if (this._animationTimeRestarted) { if (this._animationTimeRestarted) {
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = null; this._animationTimeRestarted = undefined;
if (time > 0) { if (time > 0) {
this._restartInterval(time); this._restartInterval(time);
return; return;
@@ -314,7 +320,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = false; this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => { this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback(); this._renderCallback();
this._animationFrame = null; this._animationFrame = undefined;
}); });
// Setup the blink interval // Setup the blink interval
@@ -324,7 +330,7 @@ class CursorBlinkStateManager {
// calc time diff // calc time diff
// Make restart interval do a setTimeout initially? // Make restart interval do a setTimeout initially?
const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted); const time = BLINK_INTERVAL - (Date.now() - this._animationTimeRestarted);
this._animationTimeRestarted = null; this._animationTimeRestarted = undefined;
this._restartInterval(time); this._restartInterval(time);
return; return;
} }
@@ -333,7 +339,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = !this.isCursorVisible; this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => { this._animationFrame = window.requestAnimationFrame(() => {
this._renderCallback(); this._renderCallback();
this._animationFrame = null; this._animationFrame = undefined;
}); });
}, BLINK_INTERVAL); }, BLINK_INTERVAL);
}, timeToStart); }, timeToStart);
@@ -343,20 +349,20 @@ class CursorBlinkStateManager {
this.isCursorVisible = true; this.isCursorVisible = true;
if (this._blinkInterval) { if (this._blinkInterval) {
window.clearInterval(this._blinkInterval); window.clearInterval(this._blinkInterval);
this._blinkInterval = null; this._blinkInterval = undefined;
} }
if (this._blinkStartTimeout) { if (this._blinkStartTimeout) {
window.clearTimeout(this._blinkStartTimeout); window.clearTimeout(this._blinkStartTimeout);
this._blinkStartTimeout = null; this._blinkStartTimeout = undefined;
} }
if (this._animationFrame) { if (this._animationFrame) {
window.cancelAnimationFrame(this._animationFrame); window.cancelAnimationFrame(this._animationFrame);
this._animationFrame = null; this._animationFrame = undefined;
} }
} }
public resume(): void { public resume(): void {
this._animationTimeRestarted = null; this._animationTimeRestarted = undefined;
this._restartInterval(); this._restartInterval();
this.restartBlinkAnimation(); this.restartBlinkAnimation();
} }
@@ -3,18 +3,18 @@
* @license MIT * @license MIT
*/ */
import { TextRenderLayer } from '../browser/renderer/TextRenderLayer'; import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
import { SelectionRenderLayer } from '../browser/renderer/SelectionRenderLayer'; import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer'; import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry } from 'browser/renderer/Types'; import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { ITerminal } from '../Types'; import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
import { LinkRenderLayer } from '../browser/renderer/LinkRenderLayer';
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry'; import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/Lifecycle';
import { IColorSet } from 'browser/Types'; import { IColorSet, ILinkifier } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services'; import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
import { IBufferService, IOptionsService } from 'common/services/Services'; import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
import { EventEmitter, IEvent } from 'common/EventEmitter';
let nextRendererId = 1; let nextRendererId = 1;
@@ -27,36 +27,42 @@ export class Renderer extends Disposable implements IRenderer {
public dimensions: IRenderDimensions; public dimensions: IRenderDimensions;
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
constructor( constructor(
private _colors: IColorSet, private _colors: IColorSet,
private readonly _terminal: ITerminal, private readonly _screenElement: HTMLElement,
readonly bufferService: IBufferService, private readonly _linkifier: ILinkifier,
private readonly _charSizeService: ICharSizeService, @IBufferService private readonly _bufferService: IBufferService,
readonly optionsService: IOptionsService @ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@ICoreService readonly coreService: ICoreService,
@ICoreBrowserService readonly coreBrowserService: ICoreBrowserService
) { ) {
super(); super();
const allowTransparency = this._terminal.options.allowTransparency; const allowTransparency = this._optionsService.options.allowTransparency;
this._characterJoinerRegistry = new CharacterJoinerRegistry(bufferService); this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
this._renderLayers = [ this._renderLayers = [
new TextRenderLayer(this._terminal.screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, bufferService, optionsService), new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
new SelectionRenderLayer(this._terminal.screenElement, 1, this._colors, this._id, bufferService, optionsService), new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
new LinkRenderLayer(this._terminal.screenElement, 2, this._colors, this._id, this._terminal.linkifier, bufferService, optionsService), new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, this._linkifier, this._bufferService, _optionsService),
new CursorRenderLayer(this._terminal.screenElement, 3, this._colors, this._terminal, this._id, bufferService, optionsService) new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRefreshRows, this._bufferService, _optionsService, coreService, coreBrowserService)
]; ];
this.dimensions = { this.dimensions = {
scaledCharWidth: null, scaledCharWidth: 0,
scaledCharHeight: null, scaledCharHeight: 0,
scaledCellWidth: null, scaledCellWidth: 0,
scaledCellHeight: null, scaledCellHeight: 0,
scaledCharLeft: null, scaledCharLeft: 0,
scaledCharTop: null, scaledCharTop: 0,
scaledCanvasWidth: null, scaledCanvasWidth: 0,
scaledCanvasHeight: null, scaledCanvasHeight: 0,
canvasWidth: null, canvasWidth: 0,
canvasHeight: null, canvasHeight: 0,
actualCellWidth: null, actualCellWidth: 0,
actualCellHeight: null actualCellHeight: 0
}; };
this._devicePixelRatio = window.devicePixelRatio; this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions(); this._updateDimensions();
@@ -74,7 +80,7 @@ export class Renderer extends Disposable implements IRenderer {
// and the terminal needs to refreshed // and the terminal needs to refreshed
if (this._devicePixelRatio !== window.devicePixelRatio) { if (this._devicePixelRatio !== window.devicePixelRatio) {
this._devicePixelRatio = window.devicePixelRatio; this._devicePixelRatio = window.devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows); this.onResize(this._bufferService.cols, this._bufferService.rows);
} }
} }
@@ -96,12 +102,12 @@ export class Renderer extends Disposable implements IRenderer {
this._renderLayers.forEach(l => l.resize(this.dimensions)); this._renderLayers.forEach(l => l.resize(this.dimensions));
// Resize the screen // Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
} }
public onCharSizeChanged(): void { public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows); this.onResize(this._bufferService.cols, this._bufferService.rows);
} }
public onBlur(): void { public onBlur(): void {
@@ -163,23 +169,23 @@ export class Renderer extends Disposable implements IRenderer {
// will be floored because since lineHeight can never be lower then 1, there // will be floored because since lineHeight can never be lower then 1, there
// is a guarentee that the scaled line height will always be larger than // is a guarentee that the scaled line height will always be larger than
// scaled char height. // scaled char height.
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
// Calculate the y coordinate within a cell that text should draw from in // Calculate the y coordinate within a cell that text should draw from in
// order to draw in the center of a cell. // order to draw in the center of a cell.
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2); this.dimensions.scaledCharTop = this._optionsService.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
// Calculate the scaled cell width, taking the letterSpacing into account. // Calculate the scaled cell width, taking the letterSpacing into account.
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
// Calculate the x coordinate with a cell that text should draw from in // Calculate the x coordinate with a cell that text should draw from in
// order to draw in the center of a cell. // order to draw in the center of a cell.
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2); this.dimensions.scaledCharLeft = Math.floor(this._optionsService.options.letterSpacing / 2);
// Recalculate the canvas dimensions; scaled* define the actual number of // Recalculate the canvas dimensions; scaled* define the actual number of
// pixel in the canvas // pixel in the canvas
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight; this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth; this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
// The the size of the canvas on the page. It's very important that this // The the size of the canvas on the page. It's very important that this
// rounds to nearest integer and not ceils as browsers often set // rounds to nearest integer and not ceils as browsers often set
@@ -194,8 +200,8 @@ export class Renderer extends Disposable implements IRenderer {
// account window.devicePixelRatio. ICharSizeService.width/height by itself // account window.devicePixelRatio. ICharSizeService.width/height by itself
// is insufficient when the page is not at 100% zoom level as it's measured // is insufficient when the page is not at 100% zoom level as it's measured
// in CSS pixels, but the actual char size on the canvas can differ. // in CSS pixels, but the actual char size on the canvas can differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
} }
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
+5 -1
View File
@@ -227,7 +227,11 @@ export class TextRenderLayer extends BaseRenderLayer {
} else if (cell.isBgRGB()) { } else if (cell.isBgRGB()) {
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`; this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
} else { } else {
this._ctx.fillStyle = this._colors.ansi[cell.getBgColor()].css; let bg = cell.getBgColor();
if (this._optionsService.options.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
bg += 8;
}
this._ctx.fillStyle = this._colors.ansi[bg].css;
} }
} else { } else {
if (cell.isFgDefault()) { if (cell.isFgDefault()) {
+8
View File
@@ -5,6 +5,7 @@
import { IDisposable } from 'common/Types'; import { IDisposable } from 'common/Types';
import { IColorSet } from 'browser/Types'; import { IColorSet } from 'browser/Types';
import { IEvent } from 'common/EventEmitter';
export type CharacterJoinerHandler = (text: string) => [number, number][]; export type CharacterJoinerHandler = (text: string) => [number, number][];
@@ -23,6 +24,11 @@ export interface IRenderDimensions {
actualCellHeight: number; actualCellHeight: number;
} }
export interface IRequestRefreshRowsEvent {
start: number;
end: number;
}
/** /**
* Note that IRenderer implementations should emit the refresh event after * Note that IRenderer implementations should emit the refresh event after
* rendering rows to the screen. * rendering rows to the screen.
@@ -30,6 +36,8 @@ export interface IRenderDimensions {
export interface IRenderer extends IDisposable { export interface IRenderer extends IDisposable {
readonly dimensions: IRenderDimensions; readonly dimensions: IRenderDimensions;
readonly onRequestRefreshRows: IEvent<IRequestRefreshRowsEvent>;
dispose(): void; dispose(): void;
setColors(colors: IColorSet): void; setColors(colors: IColorSet): void;
onDevicePixelRatioChange(): void; onDevicePixelRatioChange(): void;
@@ -11,6 +11,7 @@ import { LRUMap } from 'browser/renderer/atlas/LRUMap';
import { isFirefox, isSafari } from 'common/Platform'; import { isFirefox, isSafari } from 'common/Platform';
import { IColor } from 'browser/Types'; import { IColor } from 'browser/Types';
import { throwIfFalsy } from 'browser/renderer/RendererUtils'; import { throwIfFalsy } from 'browser/renderer/RendererUtils';
import { opaque } from 'browser/Color';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes, // In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works. // however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
@@ -222,7 +223,7 @@ export class DynamicCharAtlas extends BaseCharAtlas {
private _getForegroundColor(glyph: IGlyphIdentifier): IColor { private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
if (glyph.fg === INVERTED_DEFAULT_COLOR) { if (glyph.fg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.background; return opaque(this._config.colors.background);
} else if (glyph.fg < 256) { } else if (glyph.fg < 256) {
// 256 color support // 256 color support
return this._getColorFromAnsiIndex(glyph.fg); return this._getColorFromAnsiIndex(glyph.fg);
@@ -3,14 +3,15 @@
* @license MIT * @license MIT
*/ */
import { IRenderer, IRenderDimensions, CharacterJoinerHandler } from 'browser/renderer/Types'; import { IRenderer, IRenderDimensions, CharacterJoinerHandler, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
import { ITerminal } from '../../Types';
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { Disposable } from 'common/Lifecycle'; import { Disposable } from 'common/Lifecycle';
import { IColorSet, ILinkifierEvent } from 'browser/Types'; import { IColorSet, ILinkifierEvent, ILinkifier } from 'browser/Types';
import { ICharSizeService } from 'browser/services/Services'; import { ICharSizeService } from 'browser/services/Services';
import { IOptionsService } from 'common/services/Services'; import { IOptionsService, IBufferService } from 'common/services/Services';
import { EventEmitter, IEvent } from 'common/EventEmitter';
import { opaque } from 'browser/Color';
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-'; const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
const ROW_CONTAINER_CLASS = 'xterm-rows'; const ROW_CONTAINER_CLASS = 'xterm-rows';
@@ -30,19 +31,26 @@ export class DomRenderer extends Disposable implements IRenderer {
private _rowFactory: DomRendererRowFactory; private _rowFactory: DomRendererRowFactory;
private _terminalClass: number = nextTerminalId++; private _terminalClass: number = nextTerminalId++;
private _themeStyleElement: HTMLStyleElement; private _themeStyleElement!: HTMLStyleElement;
private _dimensionsStyleElement: HTMLStyleElement; private _dimensionsStyleElement!: HTMLStyleElement;
private _rowContainer: HTMLElement; private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[] = []; private _rowElements: HTMLElement[] = [];
private _selectionContainer: HTMLElement; private _selectionContainer: HTMLElement;
public dimensions: IRenderDimensions; public dimensions: IRenderDimensions;
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
constructor( constructor(
private _terminal: ITerminal,
private _colors: IColorSet, private _colors: IColorSet,
private _charSizeService: ICharSizeService, private readonly _element: HTMLElement,
private _optionsService: IOptionsService private readonly _screenElement: HTMLElement,
private readonly _viewportElement: HTMLElement,
private readonly _linkifier: ILinkifier,
@ICharSizeService private readonly _charSizeService: ICharSizeService,
@IOptionsService private readonly _optionsService: IOptionsService,
@IBufferService private readonly _bufferService: IBufferService
) { ) {
super(); super();
@@ -50,60 +58,60 @@ export class DomRenderer extends Disposable implements IRenderer {
this._rowContainer.classList.add(ROW_CONTAINER_CLASS); this._rowContainer.classList.add(ROW_CONTAINER_CLASS);
this._rowContainer.style.lineHeight = 'normal'; this._rowContainer.style.lineHeight = 'normal';
this._rowContainer.setAttribute('aria-hidden', 'true'); this._rowContainer.setAttribute('aria-hidden', 'true');
this._refreshRowElements(this._terminal.cols, this._terminal.rows); this._refreshRowElements(this._bufferService.cols, this._bufferService.rows);
this._selectionContainer = document.createElement('div'); this._selectionContainer = document.createElement('div');
this._selectionContainer.classList.add(SELECTION_CLASS); this._selectionContainer.classList.add(SELECTION_CLASS);
this._selectionContainer.setAttribute('aria-hidden', 'true'); this._selectionContainer.setAttribute('aria-hidden', 'true');
this.dimensions = { this.dimensions = {
scaledCharWidth: null, scaledCharWidth: 0,
scaledCharHeight: null, scaledCharHeight: 0,
scaledCellWidth: null, scaledCellWidth: 0,
scaledCellHeight: null, scaledCellHeight: 0,
scaledCharLeft: null, scaledCharLeft: 0,
scaledCharTop: null, scaledCharTop: 0,
scaledCanvasWidth: null, scaledCanvasWidth: 0,
scaledCanvasHeight: null, scaledCanvasHeight: 0,
canvasWidth: null, canvasWidth: 0,
canvasHeight: null, canvasHeight: 0,
actualCellWidth: null, actualCellWidth: 0,
actualCellHeight: null actualCellHeight: 0
}; };
this._updateDimensions(); this._updateDimensions();
this._injectCss(); this._injectCss();
this._rowFactory = new DomRendererRowFactory(document, this._optionsService); this._rowFactory = new DomRendererRowFactory(document, this._optionsService, this._colors);
this._terminal.element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass); this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._rowContainer);
this._terminal.screenElement.appendChild(this._selectionContainer); this._screenElement.appendChild(this._selectionContainer);
this._terminal.linkifier.onLinkHover(e => this._onLinkHover(e)); this._linkifier.onLinkHover(e => this._onLinkHover(e));
this._terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e)); this._linkifier.onLinkLeave(e => this._onLinkLeave(e));
} }
public dispose(): void { public dispose(): void {
this._terminal.element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass); this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
this._terminal.screenElement.removeChild(this._rowContainer); this._screenElement.removeChild(this._rowContainer);
this._terminal.screenElement.removeChild(this._selectionContainer); this._screenElement.removeChild(this._selectionContainer);
this._terminal.screenElement.removeChild(this._themeStyleElement); this._screenElement.removeChild(this._themeStyleElement);
this._terminal.screenElement.removeChild(this._dimensionsStyleElement); this._screenElement.removeChild(this._dimensionsStyleElement);
super.dispose(); super.dispose();
} }
private _updateDimensions(): void { private _updateDimensions(): void {
this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio; this.dimensions.scaledCharWidth = this._charSizeService.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio); this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing); this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight); this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.options.lineHeight);
this.dimensions.scaledCharLeft = 0; this.dimensions.scaledCharLeft = 0;
this.dimensions.scaledCharTop = 0; this.dimensions.scaledCharTop = 0;
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._terminal.cols; this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._terminal.rows; this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio); this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / window.devicePixelRatio);
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio); this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / window.devicePixelRatio);
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols; this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows; this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
this._rowElements.forEach(element => { this._rowElements.forEach(element => {
element.style.width = `${this.dimensions.canvasWidth}px`; element.style.width = `${this.dimensions.canvasWidth}px`;
@@ -115,7 +123,7 @@ export class DomRenderer extends Disposable implements IRenderer {
if (!this._dimensionsStyleElement) { if (!this._dimensionsStyleElement) {
this._dimensionsStyleElement = document.createElement('style'); this._dimensionsStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._dimensionsStyleElement); this._screenElement.appendChild(this._dimensionsStyleElement);
} }
const styles = const styles =
@@ -128,9 +136,9 @@ export class DomRenderer extends Disposable implements IRenderer {
this._dimensionsStyleElement.innerHTML = styles; this._dimensionsStyleElement.innerHTML = styles;
this._selectionContainer.style.height = (<any>this._terminal)._viewportElement.style.height; this._selectionContainer.style.height = this._viewportElement.style.height;
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`; this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`; this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
} }
public setColors(colors: IColorSet): void { public setColors(colors: IColorSet): void {
@@ -141,7 +149,7 @@ export class DomRenderer extends Disposable implements IRenderer {
private _injectCss(): void { private _injectCss(): void {
if (!this._themeStyleElement) { if (!this._themeStyleElement) {
this._themeStyleElement = document.createElement('style'); this._themeStyleElement = document.createElement('style');
this._terminal.screenElement.appendChild(this._themeStyleElement); this._screenElement.appendChild(this._themeStyleElement);
} }
// Base CSS // Base CSS
@@ -149,16 +157,16 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this._colors.foreground.css};` + ` color: ${this._colors.foreground.css};` +
` background-color: ${this._colors.background.css};` + ` background-color: ${this._colors.background.css};` +
` font-family: ${this._terminal.options.fontFamily};` + ` font-family: ${this._optionsService.options.fontFamily};` +
` font-size: ${this._terminal.options.fontSize}px;` + ` font-size: ${this._optionsService.options.fontSize}px;` +
`}`; `}`;
// Text styles // Text styles
styles += styles +=
`${this._terminalSelector} span:not(.${BOLD_CLASS}) {` + `${this._terminalSelector} span:not(.${BOLD_CLASS}) {` +
` font-weight: ${this._terminal.options.fontWeight};` + ` font-weight: ${this._optionsService.options.fontWeight};` +
`}` + `}` +
`${this._terminalSelector} span.${BOLD_CLASS} {` + `${this._terminalSelector} span.${BOLD_CLASS} {` +
` font-weight: ${this._terminal.options.fontWeightBold};` + ` font-weight: ${this._optionsService.options.fontWeightBold};` +
`}` + `}` +
`${this._terminalSelector} span.${ITALIC_CLASS} {` + `${this._terminalSelector} span.${ITALIC_CLASS} {` +
` font-style: italic;` + ` font-style: italic;` +
@@ -183,7 +191,7 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`; `}`;
// Cursor // Cursor
styles += styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS} {` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
` outline: 1px solid ${this._colors.cursor.css};` + ` outline: 1px solid ${this._colors.cursor.css};` +
` outline-offset: -1px;` + ` outline-offset: -1px;` +
`}` + `}` +
@@ -197,10 +205,10 @@ export class DomRenderer extends Disposable implements IRenderer {
` background-color: ${this._colors.cursor.css};` + ` background-color: ${this._colors.cursor.css};` +
` color: ${this._colors.cursorAccent.css};` + ` color: ${this._colors.cursorAccent.css};` +
`}` + `}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` + ` box-shadow: 1px 0 0 ${this._colors.cursor.css} inset;` +
`}` + `}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` + ` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
`}`; `}`;
// Selection // Selection
@@ -223,7 +231,7 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`; `${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
}); });
styles += styles +=
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${this._colors.background.css}; }` + `${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${opaque(this._colors.background).css}; }` +
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`; `${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
this._themeStyleElement.innerHTML = styles; this._themeStyleElement.innerHTML = styles;
@@ -242,7 +250,7 @@ export class DomRenderer extends Disposable implements IRenderer {
} }
// Remove excess elements // Remove excess elements
while (this._rowElements.length > rows) { while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()); this._rowContainer.removeChild(this._rowElements.pop()!);
} }
} }
@@ -275,13 +283,13 @@ export class DomRenderer extends Disposable implements IRenderer {
} }
// Translate from buffer position to viewport position // Translate from buffer position to viewport position
const viewportStartRow = start[1] - this._terminal.buffer.ydisp; const viewportStartRow = start[1] - this._bufferService.buffer.ydisp;
const viewportEndRow = end[1] - this._terminal.buffer.ydisp; const viewportEndRow = end[1] - this._bufferService.buffer.ydisp;
const viewportCappedStartRow = Math.max(viewportStartRow, 0); const viewportCappedStartRow = Math.max(viewportStartRow, 0);
const viewportCappedEndRow = Math.min(viewportEndRow, this._terminal.rows - 1); const viewportCappedEndRow = Math.min(viewportEndRow, this._bufferService.rows - 1);
// No need to draw the selection // No need to draw the selection
if (viewportCappedStartRow >= this._terminal.rows || viewportCappedEndRow < 0) { if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
return; return;
} }
@@ -295,15 +303,15 @@ export class DomRenderer extends Disposable implements IRenderer {
} else { } else {
// Draw first row // Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0; const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._terminal.cols; const endCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol)); documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow, startCol, endCol));
// Draw middle rows // Draw middle rows
const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1; const middleRowsCount = viewportCappedEndRow - viewportCappedStartRow - 1;
documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._terminal.cols, middleRowsCount)); documentFragment.appendChild(this._createSelectionElement(viewportCappedStartRow + 1, 0, this._bufferService.cols, middleRowsCount));
// Draw final row // Draw final row
if (viewportCappedStartRow !== viewportCappedEndRow) { if (viewportCappedStartRow !== viewportCappedEndRow) {
// Only draw viewportEndRow if it's not the same as viewporttartRow // Only draw viewportEndRow if it's not the same as viewporttartRow
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._terminal.cols; const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : this._bufferService.cols;
documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol)); documentFragment.appendChild(this._createSelectionElement(viewportCappedEndRow, 0, endCol));
} }
} }
@@ -333,7 +341,6 @@ export class DomRenderer extends Disposable implements IRenderer {
// Force a refresh // Force a refresh
this._updateDimensions(); this._updateDimensions();
this._injectCss(); this._injectCss();
this._terminal.refresh(0, this._terminal.rows - 1);
} }
public clear(): void { public clear(): void {
@@ -341,20 +348,18 @@ export class DomRenderer extends Disposable implements IRenderer {
} }
public renderRows(start: number, end: number): void { public renderRows(start: number, end: number): void {
const terminal = this._terminal; const cursorAbsoluteY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
const cursorX = this._bufferService.buffer.x;
const cursorAbsoluteY = terminal.buffer.ybase + terminal.buffer.y; const cursorBlink = this._optionsService.options.cursorBlink;
const cursorX = this._terminal.buffer.x;
const cursorBlink = this._terminal.options.cursorBlink;
for (let y = start; y <= end; y++) { for (let y = start; y <= end; y++) {
const rowElement = this._rowElements[y]; const rowElement = this._rowElements[y];
rowElement.innerHTML = ''; rowElement.innerHTML = '';
const row = y + terminal.buffer.ydisp; const row = y + this._bufferService.buffer.ydisp;
const lineData = terminal.buffer.lines.get(row); const lineData = this._bufferService.buffer.lines.get(row);
const cursorStyle = terminal.options.cursorStyle; const cursorStyle = this._optionsService.options.cursorStyle;
rowElement.appendChild(this._rowFactory.createRow(lineData, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, terminal.cols)); rowElement.appendChild(this._rowFactory.createRow(lineData!, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
} }
} }
@@ -11,6 +11,7 @@ import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { IBufferLine } from 'common/Types'; import { IBufferLine } from 'common/Types';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { MockOptionsService } from 'common/TestUtils.test'; import { MockOptionsService } from 'common/TestUtils.test';
import { fromCss } from 'browser/Color';
describe('DomRendererRowFactory', () => { describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM; let dom: jsdom.JSDOM;
@@ -19,7 +20,30 @@ describe('DomRendererRowFactory', () => {
beforeEach(() => { beforeEach(() => {
dom = new jsdom.JSDOM(''); dom = new jsdom.JSDOM('');
rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true })); rowFactory = new DomRendererRowFactory(dom.window.document, new MockOptionsService({ drawBoldTextInBrightColors: true }), {
background: fromCss('#010101'),
foreground: fromCss('#020202'),
ansi: [
// dark:
fromCss('#2e3436'),
fromCss('#cc0000'),
fromCss('#4e9a06'),
fromCss('#c4a000'),
fromCss('#3465a4'),
fromCss('#75507b'),
fromCss('#06989a'),
fromCss('#d3d7cf'),
// bright:
fromCss('#555753'),
fromCss('#ef2929'),
fromCss('#8ae234'),
fromCss('#fce94f'),
fromCss('#729fcf'),
fromCss('#ad7fa8'),
fromCss('#34e2e2'),
fromCss('#eeeeec')
]
} as any);
lineData = createEmptyLineData(2); lineData = createEmptyLineData(2);
}); });
@@ -142,7 +166,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell); lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment), assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bg-2 xterm-fg-1">a</span>' '<span class="xterm-fg-1 xterm-bg-2">a</span>'
); );
}); });
@@ -153,7 +177,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell); lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment), assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bg-257 xterm-fg-1">a</span>' '<span class="xterm-fg-1 xterm-bg-257">a</span>'
); );
}); });
@@ -163,7 +187,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell); lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment), assert.equal(getFragmentHtml(fragment),
'<span class="xterm-bg-1 xterm-fg-257">a</span>' '<span class="xterm-fg-257 xterm-bg-1">a</span>'
); );
}); });
@@ -188,7 +212,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell); lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment), assert.equal(getFragmentHtml(fragment),
'<span style="color:rgb(1,2,3);background-color:rgb(4,5,6);">a</span>' '<span style="color:#010203;background-color:#040506;">a</span>'
); );
}); });
@@ -199,7 +223,7 @@ describe('DomRendererRowFactory', () => {
lineData.setCell(0, cell); lineData.setCell(0, cell);
const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20); const fragment = rowFactory.createRow(lineData, false, undefined, 0, false, 5, 20);
assert.equal(getFragmentHtml(fragment), assert.equal(getFragmentHtml(fragment),
'<span style="background-color:rgb(1,2,3);color:rgb(4,5,6);">a</span>' '<span style="color:#040506;background-color:#010203;">a</span>'
); );
}); });
}); });
+100 -29
View File
@@ -5,10 +5,11 @@
import { IBufferLine } from 'common/Types'; import { IBufferLine } from 'common/Types';
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { ITerminalOptions, IOptionsService } from 'common/services/Services'; import { IOptionsService } from 'common/services/Services';
import { ensureContrastRatio, rgbaToColor } from 'browser/Color';
import { IColorSet, IColor } from 'browser/Types';
export const BOLD_CLASS = 'xterm-bold'; export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim'; export const DIM_CLASS = 'xterm-dim';
@@ -24,11 +25,16 @@ export class DomRendererRowFactory {
private _workCell: CellData = new CellData(); private _workCell: CellData = new CellData();
constructor( constructor(
private _document: Document, private readonly _document: Document,
private _optionsService: IOptionsService private readonly _optionsService: IOptionsService,
private _colors: IColorSet
) { ) {
} }
public setColors(colors: IColorSet): void {
this._colors = colors;
}
public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment { public createRow(lineData: IBufferLine, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
const fragment = this._document.createDocumentFragment(); const fragment = this._document.createDocumentFragment();
@@ -97,36 +103,101 @@ export class DomRendererRowFactory {
charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR; charElement.textContent = this._workCell.getChars() || WHITESPACE_CELL_CHAR;
const swapColor = this._workCell.isInverse(); let fg = this._workCell.getFgColor();
let fgColorMode = this._workCell.getFgColorMode();
// fg let bg = this._workCell.getBgColor();
if (this._workCell.isFgRGB()) { let bgColorMode = this._workCell.getBgColorMode();
let style = charElement.getAttribute('style') || ''; const isInverse = !!this._workCell.isInverse();
style += `${swapColor ? 'background-' : ''}color:rgb(${(AttributeData.toColorRGB(this._workCell.getFgColor())).join(',')});`; if (isInverse) {
charElement.setAttribute('style', style); const temp = fg;
} else if (this._workCell.isFgPalette()) { fg = bg;
let fg = this._workCell.getFgColor(); bg = temp;
if (this._workCell.isBold() && fg < 8 && !swapColor && this._optionsService.options.drawBoldTextInBrightColors) { const temp2 = fgColorMode;
fg += 8; fgColorMode = bgColorMode;
} bgColorMode = temp2;
charElement.classList.add(`xterm-${swapColor ? 'b' : 'f'}g-${fg}`);
} else if (swapColor) {
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
} }
// bg // Foreground
if (this._workCell.isBgRGB()) { switch (fgColorMode) {
let style = charElement.getAttribute('style') || ''; case Attributes.CM_P16:
style += `${swapColor ? '' : 'background-'}color:rgb(${(AttributeData.toColorRGB(this._workCell.getBgColor())).join(',')});`; case Attributes.CM_P256:
charElement.setAttribute('style', style); if (this._workCell.isBold() && fg < 8 && this._optionsService.options.drawBoldTextInBrightColors) {
} else if (this._workCell.isBgPalette()) { fg += 8;
charElement.classList.add(`xterm-${swapColor ? 'f' : 'b'}g-${this._workCell.getBgColor()}`); }
} else if (swapColor) { if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.ansi[fg])) {
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`); charElement.classList.add(`xterm-fg-${fg}`);
}
break;
case Attributes.CM_RGB:
const color = rgbaToColor(
(fg >> 16) & 0xFF,
(fg >> 8) & 0xFF,
(fg ) & 0xFF
);
if (!this._applyMinimumContrast(charElement, this._colors.background, color)) {
this._addStyle(charElement, `color:#${padStart(fg.toString(16), '0', 6)}`);
}
break;
case Attributes.CM_DEFAULT:
default:
if (!this._applyMinimumContrast(charElement, this._colors.background, this._colors.foreground)) {
if (isInverse) {
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
}
}
}
// Background
switch (bgColorMode) {
case Attributes.CM_P16:
case Attributes.CM_P256:
charElement.classList.add(`xterm-bg-${bg}`);
break;
case Attributes.CM_RGB:
this._addStyle(charElement, `background-color:#${padStart(bg.toString(16), '0', 6)}`);
break;
case Attributes.CM_DEFAULT:
default:
if (isInverse) {
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
}
} }
fragment.appendChild(charElement); fragment.appendChild(charElement);
} }
return fragment; return fragment;
} }
private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor): boolean {
if (this._optionsService.options.minimumContrastRatio === 1) {
return false;
}
// Try get from cache first
let adjustedColor = this._colors.contrastCache.getColor(this._workCell.bg, this._workCell.fg);
// Calculate and store in cache
if (adjustedColor === undefined) {
adjustedColor = ensureContrastRatio(bg, fg, this._optionsService.options.minimumContrastRatio);
this._colors.contrastCache.setColor(this._workCell.bg, this._workCell.fg, adjustedColor ?? null);
}
if (adjustedColor) {
this._addStyle(element, `color:${adjustedColor.css}`);
return true;
}
return false;
}
private _addStyle(element: HTMLElement, style: string): void {
element.setAttribute('style', `${element.getAttribute('style') || ''}${style};`);
}
}
function padStart(text: string, padChar: string, length: number): string {
while (text.length < length) {
text = padChar + text;
}
return text;
} }
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICoreBrowserService } from './Services';
export class CoreBrowserService implements ICoreBrowserService {
serviceBrand: any;
constructor(
private _textarea: HTMLTextAreaElement
) {
}
public get isFocused(): boolean {
return document.activeElement === this._textarea && document.hasFocus();
}
}
+6
View File
@@ -51,6 +51,9 @@ export class RenderService extends Disposable implements IRenderService {
this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged())); this.register(optionsService.onOptionChange(() => this._renderer.onOptionsChanged()));
this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged())); this.register(charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
// No need to register this as renderer is explicitly disposed in RenderService.dispose
this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end));
// dprchange should handle this case, we need this as well for browsers that don't support the // dprchange should handle this case, we need this as well for browsers that don't support the
// matchMedia query. // matchMedia query.
this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange())); this.register(addDisposableDomListener(window, 'resize', () => this.onDevicePixelRatioChange()));
@@ -92,6 +95,7 @@ export class RenderService extends Disposable implements IRenderService {
public changeOptions(): void { public changeOptions(): void {
this._renderer.onOptionsChanged(); this._renderer.onOptionsChanged();
this.refreshRows(0, this._rowCount - 1);
this._fireOnCanvasResize(); this._fireOnCanvasResize();
} }
@@ -105,12 +109,14 @@ export class RenderService extends Disposable implements IRenderService {
public dispose(): void { public dispose(): void {
this._renderer.dispose(); this._renderer.dispose();
super.dispose();
} }
public setRenderer(renderer: IRenderer): void { public setRenderer(renderer: IRenderer): void {
// TODO: RenderService should be the only one to dispose the renderer // TODO: RenderService should be the only one to dispose the renderer
this._renderer.dispose(); this._renderer.dispose();
this._renderer = renderer; this._renderer = renderer;
this._renderer.onRequestRefreshRows(e => this.refreshRows(e.start, e.end));
this.refreshRows(0, this._rowCount - 1); this.refreshRows(0, this._rowCount - 1);
} }
+3 -5
View File
@@ -210,7 +210,7 @@ export class SelectionService implements ISelectionService {
for (let i = start[1] + 1; i <= end[1] - 1; i++) { for (let i = start[1] + 1; i <= end[1] - 1; i++) {
const bufferLine = buffer.lines.get(i); const bufferLine = buffer.lines.get(i);
const lineText = buffer.translateBufferLineToString(i, true); const lineText = buffer.translateBufferLineToString(i, true);
if (bufferLine!.isWrapped) { if (bufferLine && bufferLine.isWrapped) {
result[result.length - 1] += lineText; result[result.length - 1] += lineText;
} else { } else {
result.push(lineText); result.push(lineText);
@@ -221,7 +221,7 @@ export class SelectionService implements ISelectionService {
if (start[1] !== end[1]) { if (start[1] !== end[1]) {
const bufferLine = buffer.lines.get(end[1]); const bufferLine = buffer.lines.get(end[1]);
const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]); const lineText = buffer.translateBufferLineToString(end[1], true, 0, end[0]);
if (bufferLine!.isWrapped) { if (bufferLine && bufferLine!.isWrapped) {
result[result.length - 1] += lineText; result[result.length - 1] += lineText;
} else { } else {
result.push(lineText); result.push(lineText);
@@ -688,9 +688,7 @@ export class SelectionService implements ISelectionService {
// reverseIndex) and delete in a splice is only ever used when the same // reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be // number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases. // beneficial to leave the selection as is for these cases.
if (this._trimListener) { this._trimListener.dispose();
this._trimListener.dispose();
}
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount)); this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
} }
+7
View File
@@ -23,6 +23,13 @@ export interface ICharSizeService {
measure(): void; measure(): void;
} }
export const ICoreBrowserService = createDecorator<ICoreBrowserService>('CoreBrowserService');
export interface ICoreBrowserService {
serviceBrand: any;
readonly isFocused: boolean;
}
export const IMouseService = createDecorator<IMouseService>('MouseService'); export const IMouseService = createDecorator<IMouseService>('MouseService');
export interface IMouseService { export interface IMouseService {
serviceBrand: any; serviceBrand: any;
+15 -4
View File
@@ -16,11 +16,13 @@ export interface IEvent<T> {
export interface IEventEmitter<T> { export interface IEventEmitter<T> {
event: IEvent<T>; event: IEvent<T>;
fire(data: T): void; fire(data: T): void;
dispose(): void;
} }
export class EventEmitter<T> implements IEventEmitter<T> { export class EventEmitter<T> implements IEventEmitter<T> {
private _listeners: IListener<T>[] = []; private _listeners: IListener<T>[] = [];
private _event?: IEvent<T>; private _event?: IEvent<T>;
private _disposed: boolean = false;
public get event(): IEvent<T> { public get event(): IEvent<T> {
if (!this._event) { if (!this._event) {
@@ -28,10 +30,12 @@ export class EventEmitter<T> implements IEventEmitter<T> {
this._listeners.push(listener); this._listeners.push(listener);
const disposable = { const disposable = {
dispose: () => { dispose: () => {
for (let i = 0; i < this._listeners.length; i++) { if (!this._disposed) {
if (this._listeners[i] === listener) { for (let i = 0; i < this._listeners.length; i++) {
this._listeners.splice(i, 1); if (this._listeners[i] === listener) {
return; this._listeners.splice(i, 1);
return;
}
} }
} }
} }
@@ -51,4 +55,11 @@ export class EventEmitter<T> implements IEventEmitter<T> {
queue[i].call(undefined, data); queue[i].call(undefined, data);
} }
} }
public dispose(): void {
if (this._listeners) {
this._listeners.length = 0;
}
this._disposed = true;
}
} }
+5
View File
@@ -43,12 +43,17 @@ export class MockCoreMouseService implements ICoreMouseService {
} }
export class MockCoreService implements ICoreService { export class MockCoreService implements ICoreService {
isCursorInitialized: boolean = false;
isCursorHidden: boolean = false;
isFocused: boolean = false;
serviceBrand: any; serviceBrand: any;
decPrivateModes: IDecPrivateModes = {} as any; decPrivateModes: IDecPrivateModes = {} as any;
onData: IEvent<string> = new EventEmitter<string>().event; onData: IEvent<string> = new EventEmitter<string>().event;
onUserInput: IEvent<void> = new EventEmitter<void>().event; onUserInput: IEvent<void> = new EventEmitter<void>().event;
onBinary: IEvent<string> = new EventEmitter<string>().event;
reset(): void {} reset(): void {}
triggerDataEvent(data: string, wasUserInput?: boolean): void {} triggerDataEvent(data: string, wasUserInput?: boolean): void {}
triggerBinaryEvent(data: string): void {}
} }
export class MockDirtyRowService implements IDirtyRowService { export class MockDirtyRowService implements IDirtyRowService {
+3
View File
@@ -249,5 +249,8 @@ export interface ICoreMouseProtocol {
* The tracking encoding can be registered and activated at the CoreMouseService. * The tracking encoding can be registered and activated at the CoreMouseService.
* If a ICoreMouseEvent passes all procotol restrictions it will be encoded * If a ICoreMouseEvent passes all procotol restrictions it will be encoded
* with the active encoding and sent out. * with the active encoding and sent out.
* Note: Returning an empty string will supress sending a mouse report,
* which can be used to skip creating falsey reports in limited encodings
* (DEFAULT only supports up to 223 1-based as coord value).
*/ */
export type CoreMouseEncoding = (event: ICoreMouseEvent) => string; export type CoreMouseEncoding = (event: ICoreMouseEvent) => string;
@@ -3,11 +3,10 @@
* @license MIT * @license MIT
*/ */
import { IDisposable } from 'xterm';
import { ITerminal } from './Types';
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants'; import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
import { IBufferService } from 'common/services/Services';
export function applyWindowsMode(terminal: ITerminal): IDisposable { export function handleWindowsModeLineFeed(bufferService: IBufferService): void {
// Winpty does not support wraparound mode which means that lines will never // Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line // be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening // retaining the wrapped new line characters or if consumers are listening
@@ -18,11 +17,11 @@ export function applyWindowsMode(terminal: ITerminal): IDisposable {
// space. This is certainly not without its problems, but generally on // space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be // Windows when text reaches the end of the terminal it's likely going to be
// wrapped. // wrapped.
return terminal.onLineFeed(() => { const line = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y - 1);
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1); const lastChar = line?.get(bufferService.cols - 1);
const lastChar = line.get(terminal.cols - 1);
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y); const nextLine = bufferService.buffer.lines.get(bufferService.buffer.ybase + bufferService.buffer.y);
if (nextLine && lastChar) {
nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE); nextLine.isWrapped = (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE);
}); }
} }
+18 -12
View File
@@ -167,19 +167,25 @@ export class Buffer implements IBuffer {
if (this._rows < newRows) { if (this._rows < newRows) {
for (let y = this._rows; y < newRows; y++) { for (let y = this._rows; y < newRows; y++) {
if (this.lines.length < newRows + this.ybase) { if (this.lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) { if (this._optionsService.options.windowsMode) {
// There is room above the buffer and there are no empty elements below the line, // Just add the new missing rows on Windows as conpty reprints the screen with it's
// scroll up // view of the world. Once a line enters scrollback for conpty it remains there
this.ybase--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
}
} 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(new BufferLine(newCols, nullCell)); this.lines.push(new BufferLine(newCols, nullCell));
} else {
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--;
addToY++;
if (this.ydisp > 0) {
// Viewport is at the top of the buffer, must increase downwards
this.ydisp--;
}
} 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(new BufferLine(newCols, nullCell));
}
} }
} }
} }
+1 -1
View File
@@ -5,7 +5,7 @@
import { CharData, IBufferLine, ICellData } from 'common/Types'; import { CharData, IBufferLine, ICellData } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder'; import { stringFromCodePoint } from 'common/input/TextDecoder';
import { DEFAULT_COLOR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants'; import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Content } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData'; import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData'; import { AttributeData } from 'common/buffer/AttributeData';
+1
View File
@@ -29,6 +29,7 @@ export class Marker extends Disposable implements IMarker {
return; return;
} }
this.isDisposed = true; this.isDisposed = true;
this.line = -1;
// Emit before super.dispose such that dispose listeners get a change to react // Emit before super.dispose such that dispose listeners get a change to react
this._onDispose.fire(); this._onDispose.fire();
} }
+12
View File
@@ -108,6 +108,12 @@ describe('Keyboard', () => {
it('should return \\x1b[5C for alt+right', () => { it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
}); });
it('should return \\x1b[5D for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: false }).key, '\x1b[1;5A'); // CSI 5 D
});
it('should return \\x1b[5C for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: false }).key, '\x1b[1;5B'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => { it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba'); assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
}); });
@@ -120,6 +126,12 @@ describe('Keyboard', () => {
it('should return \\x1bf for alt+right', () => { it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
}); });
it('should return \\x1bb for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }, { isMac: true }).key, '\x1b[1;3A'); // CSI 5 D
});
it('should return \\x1bf for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }, { isMac: true }).key, '\x1b[1;3B'); // CSI 5 C
});
it('should return undefined for alt+a', () => { it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true }; assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
}); });
+23 -14
View File
@@ -113,13 +113,16 @@ export function evaluateKeyboardEvent(
break; break;
case 37: case 37:
// left-arrow // left-arrow
if (ev.metaKey) {
break;
}
if (modifiers) { if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards // HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106 // http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux // macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') { if (result.key === C0.ESC + '[1;3D') {
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D'; result.key = C0.ESC + (isMac ? 'b' : '[1;5D');
} }
} else if (applicationCursorMode) { } else if (applicationCursorMode) {
result.key = C0.ESC + 'OD'; result.key = C0.ESC + 'OD';
@@ -129,13 +132,16 @@ export function evaluateKeyboardEvent(
break; break;
case 39: case 39:
// right-arrow // right-arrow
if (ev.metaKey) {
break;
}
if (modifiers) { if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward // HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106 // http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux // macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') { if (result.key === C0.ESC + '[1;3C') {
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C'; result.key = C0.ESC + (isMac ? 'f' : '[1;5C');
} }
} else if (applicationCursorMode) { } else if (applicationCursorMode) {
result.key = C0.ESC + 'OC'; result.key = C0.ESC + 'OC';
@@ -145,11 +151,15 @@ export function evaluateKeyboardEvent(
break; break;
case 38: case 38:
// up-arrow // up-arrow
if (ev.metaKey) {
break;
}
if (modifiers) { if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow // HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106 // http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3A') { // macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A'; result.key = C0.ESC + '[1;5A';
} }
} else if (applicationCursorMode) { } else if (applicationCursorMode) {
@@ -160,11 +170,15 @@ export function evaluateKeyboardEvent(
break; break;
case 40: case 40:
// down-arrow // down-arrow
if (ev.metaKey) {
break;
}
if (modifiers) { if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B'; result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow // HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106 // http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3B') { // macOS uses different escape sequences than linux
if (!isMac && result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B'; result.key = C0.ESC + '[1;5B';
} }
} else if (applicationCursorMode) { } else if (applicationCursorMode) {
@@ -316,23 +330,18 @@ export function evaluateKeyboardEvent(
if (ev.keyCode >= 65 && ev.keyCode <= 90) { if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = String.fromCharCode(ev.keyCode - 64); result.key = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode === 32) { } else if (ev.keyCode === 32) {
// NUL result.key = C0.NUL;
result.key = String.fromCharCode(0);
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) { } else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
// escape, file sep, group sep, record sep, unit sep // escape, file sep, group sep, record sep, unit sep
result.key = String.fromCharCode(ev.keyCode - 51 + 27); result.key = String.fromCharCode(ev.keyCode - 51 + 27);
} else if (ev.keyCode === 56) { } else if (ev.keyCode === 56) {
// delete result.key = C0.DEL;
result.key = String.fromCharCode(127);
} else if (ev.keyCode === 219) { } else if (ev.keyCode === 219) {
// ^[ - Control Sequence Introducer (CSI) result.key = C0.ESC;
result.key = String.fromCharCode(27);
} else if (ev.keyCode === 220) { } else if (ev.keyCode === 220) {
// ^\ - String Terminator (ST) result.key = C0.FS;
result.key = String.fromCharCode(28);
} else if (ev.keyCode === 221) { } else if (ev.keyCode === 221) {
// ^] - Operating System Command (OSC) result.key = C0.GS;
result.key = String.fromCharCode(29);
} }
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) { } else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead. // On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
+11
View File
@@ -7,6 +7,7 @@ import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder'; import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/TextDecoder';
import { encode } from 'utf8'; import { encode } from 'utf8';
// convert UTF32 codepoints to string // convert UTF32 codepoints to string
function toString(data: Uint32Array, length: number): string { function toString(data: Uint32Array, length: number): string {
if ((String as any).fromCodePoint) { if ((String as any).fromCodePoint) {
@@ -214,6 +215,16 @@ describe('text encodings', () => {
} }
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
}); });
it('test break after 3 bytes - issue #2495', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xf0\xa0\x9c\x8e');
let written = decoder.decode(utf8Data.slice(0, 3), target);
assert.equal(written, 0);
written = decoder.decode(utf8Data.slice(3), target);
assert.equal(written, 1);
assert(toString(target, written), '𠜎');
});
}); });
}); });
}); });
+1 -1
View File
@@ -194,7 +194,7 @@ export class Utf8ToUtf32 {
target[size++] = cp; target[size++] = cp;
} }
} else { } else {
if (codepoint < 0x010000 || codepoint > 0x10FFFF) { if (cp < 0x010000 || cp > 0x10FFFF) {
// illegal codepoint // illegal codepoint
} else { } else {
target[size++] = cp; target[size++] = cp;
+88
View File
@@ -0,0 +1,88 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { WriteBuffer } from './WriteBuffer';
declare let Buffer: any;
function toBytes(s: string): Uint8Array {
return Buffer.from(s);
}
function fromBytes(bytes: Uint8Array): string {
return bytes.toString();
}
describe('WriteBuffer', () => {
let wb: WriteBuffer;
let stack: (string | Uint8Array)[] = [];
let cbStack: string[] = [];
beforeEach(() => {
stack = [];
cbStack = [];
wb = new WriteBuffer(data => { stack.push(data); });
});
describe('write input', () => {
it('string', done => {
wb.write('a._');
wb.write('b.x', () => { cbStack.push('b'); });
wb.write('c._');
wb.write('d.x', () => { cbStack.push('d'); });
wb.write('e', () => {
assert.deepEqual(stack, ['a._', 'b.x', 'c._', 'd.x', 'e']);
assert.deepEqual(cbStack, ['b', 'd']);
done();
});
});
it('bytes', done => {
wb.write(toBytes('a._'));
wb.write(toBytes('b.x'), () => { cbStack.push('b'); });
wb.write(toBytes('c._'));
wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
wb.write(toBytes('e'), () => {
assert.deepEqual(stack.map(val => typeof val === 'string' ? '' : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
assert.deepEqual(cbStack, ['b', 'd']);
done();
});
});
it('string/bytes mixed', done => {
wb.write('a._');
wb.write('b.x', () => { cbStack.push('b'); });
wb.write(toBytes('c._'));
wb.write(toBytes('d.x'), () => { cbStack.push('d'); });
wb.write(toBytes('e'), () => {
assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a._', 'b.x', 'c._', 'd.x', 'e']);
assert.deepEqual(cbStack, ['b', 'd']);
done();
});
});
it('write callback works for empty chunks', done => {
wb.write('a', () => { cbStack.push('a'); });
wb.write('', () => { cbStack.push('b'); });
wb.write(toBytes('c'), () => { cbStack.push('c'); });
wb.write(new Uint8Array(0), () => { cbStack.push('d'); });
wb.write('e', () => {
assert.deepEqual(stack.map(val => typeof val === 'string' ? val : fromBytes(val)), ['a', '', 'c', '', 'e']);
assert.deepEqual(cbStack, ['a', 'b', 'c', 'd']);
done();
});
});
it('writeSync', done => {
wb.write('a', () => { cbStack.push('a'); });
wb.write('b', () => { cbStack.push('b'); });
wb.write('c', () => { cbStack.push('c'); });
wb.writeSync('d');
assert.deepEqual(stack, ['a', 'b', 'c', 'd']);
assert.deepEqual(cbStack, ['a', 'b', 'c']);
wb.write('x', () => { cbStack.push('x'); });
wb.write('', () => {
assert.deepEqual(stack, ['a', 'b', 'c', 'd', 'x', '']);
assert.deepEqual(cbStack, ['a', 'b', 'c', 'x']);
done();
});
});
});
});
+110
View File
@@ -0,0 +1,110 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
declare const setTimeout: (handler: () => void, timeout?: number) => void;
/**
* Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
* Enable flow control to avoid this limit and make sure that your backend correctly
* propagates this to the underlying pty. (see docs for further instructions)
* Since this limit is meant as a safety parachute to prevent browser crashs,
* it is set to a very high number. Typically xterm.js gets unresponsive with
* a 100 times lower number (>500 kB).
*/
const DISCARD_WATERMARK = 50000000; // ~50 MB
/**
* The max number of ms to spend on writes before allowing the renderer to
* catch up with a 0ms setTimeout. A value of < 33 to keep us close to
* 30fps, and a value of < 16 to try to run at 60fps. Of course, the real FPS
* depends on the time it takes for the renderer to draw the frame.
*/
const WRITE_TIMEOUT_MS = 12;
/**
* Threshold of max held chunks in the write buffer, that were already processed.
* This is a tradeoff between extensive write buffer shifts (bad runtime) and high
* memory consumption by data thats not used anymore.
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
export class WriteBuffer {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
constructor(private _action: (data: string | Uint8Array) => void) { }
public writeSync(data: string | Uint8Array): void {
// force sync processing on pending data chunks to avoid in-band data scrambling
// does the same as innerWrite but without event loop
if (this._writeBuffer.length) {
for (let i = this._bufferOffset; i < this._writeBuffer.length; ++i) {
const data = this._writeBuffer[i];
const cb = this._callbacks[i];
this._action(data);
if (cb) cb();
}
// reset all to avoid reprocessing of chunks with scheduled innerWrite call
this._writeBuffer = [];
this._callbacks = [];
this._pendingData = 0;
// stop scheduled innerWrite by offset > length condition
this._bufferOffset = 0x7FFFFFFF;
}
// handle current data chunk
this._action(data);
}
public write(data: string | Uint8Array, callback?: () => void): void {
if (this._pendingData > DISCARD_WATERMARK) {
throw new Error('write data discarded, use flow control to avoid losing data');
}
// schedule chunk processing for next event loop run
if (!this._writeBuffer.length) {
this._bufferOffset = 0;
setTimeout(() => this._innerWrite());
}
this._pendingData += data.length;
this._writeBuffer.push(data);
this._callbacks.push(callback);
}
protected _innerWrite(): void {
const startTime = Date.now();
while (this._writeBuffer.length > this._bufferOffset) {
const data = this._writeBuffer[this._bufferOffset];
const cb = this._callbacks[this._bufferOffset];
this._bufferOffset++;
this._action(data);
this._pendingData -= data.length;
if (cb) cb();
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
}
}
if (this._writeBuffer.length > this._bufferOffset) {
// Allow renderer to catch up before processing the next batch
// trim already processed chunks if we are above threshold
if (this._bufferOffset > WRITE_BUFFER_LENGTH_THRESHOLD) {
this._writeBuffer = this._writeBuffer.slice(this._bufferOffset);
this._callbacks = this._callbacks.slice(this._bufferOffset);
this._bufferOffset = 0;
}
setTimeout(() => this._innerWrite(), 0);
} else {
this._writeBuffer = [];
this._callbacks = [];
this._pendingData = 0;
this._bufferOffset = 0;
}
}
}

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