Compare commits

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- translate newlines to \r which is what terminal programs expect
2018-02-22 23:52:45 -06:00
Bruno Ribeiro 97feb64e94 Merge branch 'master' into allow-named-theme-colors-1195 2018-02-21 23:51:36 +00:00
Daniel Imms 94bbc4e09d Merge pull request #1200 from nikonso/1181
WIP: Use web audio api
2018-02-21 15:36:19 -08:00
Daniel Imms ad7d2262ec Undo package-lock changes 2018-02-21 15:35:53 -08:00
Bruno Ribeito 1114977a21 Fix tests 2018-02-21 23:35:37 +00:00
Bruno Ribeiro a8c5b159a9 Merge branch 'master' into allow-named-theme-colors-1195 2018-02-21 23:17:00 +00:00
Daniel Imms a91e0e234a Use const everywhere 2018-02-21 14:44:48 -08:00
Daniel Imms af5b3f704e Fix lint 2018-02-21 14:42:04 -08:00
Daniel Imms 45c9763092 Fix indentation 2018-02-21 14:40:22 -08:00
Daniel Imms db8ba17147 Check audio context ctor before constructing 2018-02-21 14:39:43 -08:00
Daniel Imms 1972bf4bb3 Merge remote-tracking branch 'ups/master' into 1181 2018-02-21 14:36:11 -08:00
Daniel Imms 62cf0936dc Merge pull request #1231 from saamalik/ctrl-alt-sequences
Additional alt and control sequences
2018-02-21 14:08:31 -08:00
Bruno Ribeito a9d6686e14 Regex is not needed for color validation 2018-02-20 20:38:12 +00:00
Bruno Ribeito 99aa142cef Fix test -> wrong Regex flag 2018-02-19 23:55:25 +00:00
Bruno Ribeito f8f1355155 Validate colors 2018-02-19 22:54:09 +00:00
Daniel Imms ae28814041 Check for valid coords before getting word at coords
Fixes #1287
2018-02-16 21:34:57 -08:00
Daniel Griffen 183fd2fe79 fix terminal width calculation 2018-02-16 10:18:03 -08:00
Saad Malik 8324462b32 Merge branch 'master' into ctrl-alt-sequences 2018-02-15 09:49:16 -08:00
Daniel Imms 93e5996422 Merge pull request #1182 from Tyriar/731_screen_reader
Screen reader support
2018-02-15 06:20:14 -08:00
Daniel Imms f089b997c0 Merge branch 'master' into 731_screen_reader 2018-02-14 09:03:31 -08:00
Saad Malik 51d2064061 Merge branch 'master' into ctrl-alt-sequences 2018-02-13 14:59:46 -08:00
Daniel Imms 712b91a971 Merge pull request #1282 from matthew-matvei/add-freeman
Added FreeMAN to list of real-world uses
2018-02-13 06:37:55 -08:00
Daniel Imms cbe6c64a92 Merge pull request #1280 from AndrienkoAleksandr/fixHeadlessMode
Fix xterm.js in headless mode yields TypeError on resize
2018-02-12 12:42:14 -08:00
Matthew James 64ed8309a1 Added FreeMAN to list of real-world uses 2018-02-12 20:26:09 +00:00
Oleksandr Andriienko b5ba9ae771 Fix xterm.js in headless mode yields TypeError on resize
Signed-off-by: Oleksandr Andriienko <oandriie@redhat.com>
2018-02-12 12:30:26 +02:00
Saad Malik 18f6556061 Merge branch 'master' into ctrl-alt-sequences 2018-02-09 14:00:24 -08:00
Paris Kasidiaris 1dbe7b3a8c Bump version to 3.1.0
Signed-off-by: Paris Kasidiaris <paris@sourcelair.com>
2018-02-09 08:42:39 +00:00
Saad Malik 6dc49b7d01 Merge branch 'master' into ctrl-alt-sequences 2018-02-08 12:03:38 -08:00
Daniel Imms 987be07a70 Merge pull request #1276 from Tyriar/enableBold_deprecated
Mark enableBold setting as deprecated
2018-02-07 07:46:50 -08:00
Daniel Imms 760837e5cb Mark enableBold setting as deprecated 2018-02-06 07:25:54 -08:00
Thomas Zilz dc759a93bc Support setting padding on the .xterm element (#1208)
* Support padding on the .xterm element
2018-02-06 12:31:17 +01:00
Daniel Imms 1ec7b91a98 Merge pull request #1264 from Tyriar/1021_support_modifiers
Allow support of modifiers with links
2018-02-06 02:32:00 -08:00
Daniel Imms c218eebb24 Merge pull request #1266 from Tyriar/1265_sync_when_attached
Ensure viewport is attached before syncing scrollbar
2018-02-06 02:24:59 -08:00
Daniel Imms 8040e3de4a Merge pull request #1275 from xtermjs/Tyriar-patch-1
Update readme copyright statement
2018-02-06 02:23:39 -08:00
Paris Kasidiaris ff400bfcdc Merge pull request #1273 from xtermjs/webpack-watch
Add `watch` gulp task and npm script
2018-02-06 12:20:36 +02:00
Daniel Imms 35ea593806 Update readme copyright statement 2018-02-06 02:07:56 -08:00
Paris Kasidiaris f02903a6df Update README.md and Docker files 2018-02-06 09:22:16 +00:00
Paris Kasidiaris 182426dd0f Rename gulp task: webpack:watch → watch` and also monitor subdirectories 2018-02-06 10:28:34 +02:00
Paris Kasidiaris d0f45fee25 Fix #1232: Add webpack:watch gulp task 2018-02-05 15:49:37 +00:00
Daniel Imms 1462b30c88 Merge pull request #1271 from krasimir/patch-1
Update README.md
2018-02-05 07:41:23 -08:00
Krasimir Tsonev c302bc9f17 Update README.md
In `Getting Started` section we read `npm install` only. Shouldn't be `npm install xterm`?
2018-02-05 09:32:34 +02:00
Daniel Imms 01d841fec6 Merge pull request #1246 from Tyriar/1245_revert_page_change
Revert "Add support for modifiers for PageUp / PageDown keys."
2018-02-02 09:51:28 -08:00
Daniel Imms 1124f1fc7f Merge pull request #1114 from nikonso/feature-778
Select the word underneath the cursor on contextmenu
2018-02-01 17:35:52 -08:00
Daniel Imms cab097c09c Clean up condition for readability 2018-02-01 17:28:08 -08:00
Daniel Imms 43adf0ef70 Merge branch 'master' into feature-778 2018-02-01 17:21:04 -08:00
Daniel Imms 3c6cab1491 Clear selectionEnd when selecting word at cursor 2018-02-01 17:20:33 -08:00
Bruno Ribeito 5550bfd90b Fix is click in selection 2018-02-01 21:37:06 +00:00
Daniel Imms 04ccb638b4 Fix lint 2018-02-01 11:28:56 -08:00
Daniel Imms 5c05471993 Merge branch 'master' into 1021_support_modifiers 2018-02-01 11:28:05 -08:00
Daniel Imms 1e32d26a85 Merge branch 'master' into 1265_sync_when_attached 2018-02-01 10:28:41 -08:00
Daniel Imms 6105975308 Ensure viewport is attached before syncing scrollbar
Fixes #1265
2018-02-01 10:24:17 -08:00
Daniel Imms cd24f7a26c Merge pull request #1263 from Tyriar/ts_2.7
typescript@2.7
2018-02-01 08:16:42 -08:00
Daniel Imms 2ac5a8da49 Allow support of modifiers with links
Fixes #1021
2018-01-31 17:42:33 -08:00
Daniel Imms e849629533 typescript@2.7
Tuples using a fixed length by default will be helpful for CharData
2018-01-31 11:32:19 -08:00
Bruno Ribeito 983af8fac2 Use _selectWordA 2018-01-31 00:01:46 +00:00
Daniel Imms 74a6dfb197 Send arrow events on mouse wheel in alt buffer
Fixes #426
2018-01-29 11:28:48 -08:00
Saad Malik 73e0db1912 Merge branch 'master' into ctrl-alt-sequences 2018-01-29 10:21:36 -08:00
Daniel Imms 62d6c242fc Merge pull request #1249 from Tyriar/1248_tslint_curly
Add curly to tslint, fix errors
2018-01-29 07:54:09 -08:00
Daniel Imms e7b4e002f4 Merge remote-tracking branch 'origin/master' into 1248_tslint_curly 2018-01-29 07:45:31 -08:00
Daniel Imms d190bc0531 Merge pull request #1250 from xtermjs/Tyriar-patch-1
Add SQL Operations Studio to real world uses
2018-01-29 07:44:36 -08:00
Daniel Imms fb1d902804 Merge pull request #1258 from Tyriar/travis_tsc
Run tsc as a travis job
2018-01-29 07:44:26 -08:00
Daniel Imms 12cd6ea0e4 Merge pull request #1260 from Tyriar/typo
Fix recent typo in comment
2018-01-29 07:43:56 -08:00
Daniel Imms 69ebb3e53e Fix recent typo in comment 2018-01-29 07:42:59 -08:00
Paris Kasidiaris f2fe8ca08b Merge pull request #896 from npezza93/alt-click
Alt+click will move the prompt cursor to that position
2018-01-29 11:58:46 +02:00
Daniel Imms bbac1801e5 Small clean up 2018-01-28 17:33:51 -08:00
Daniel Imms a6a4bf0817 Merge remote-tracking branch 'ups/master' into feature-778 2018-01-28 17:25:46 -08:00
Saad Malik 9ade1f0a5d Add mapping for backward slash
* Add mapping for backward slash
* Fix mapping for single/double quote
2018-01-28 17:11:13 -08:00
Saad Malik f8fbdf31fa Merge branch 'master' into ctrl-alt-sequences 2018-01-28 16:54:55 -08:00
Daniel Imms e8666cafc9 Small clean up 2018-01-28 14:44:33 -08:00
Daniel Imms db95442af6 Merge pull request #1236 from chabou/pr/1148
Add allowTransparency option
2018-01-28 14:19:20 -08:00
Daniel Imms fc2af1d806 Fix lint, reduce duplication, don't scale ctx
Scaling ctx was breaking the scale when setOption was called, not sure
it was doing anything.
2018-01-28 14:13:25 -08:00
Daniel Imms 106704b5d8 Merge remote-tracking branch 'ups/master' into pr/1148 2018-01-28 13:39:07 -08:00
Daniel Imms a0f390ef54 Expose strings through the API 2018-01-28 13:10:11 -08:00
Daniel Imms af375eadb4 Run tsc as a travis job
This will hard fail when there are semantic errors. Currently building does
not do this and we will merge these in as a result.
2018-01-28 12:27:01 -08:00
Daniel Imms 098a3d0804 Merge pull request #1253 from sitzmar/right_click_timeout
adjust timeout to ensure right-click event has fired
2018-01-28 10:35:44 -08:00
npezza93 3bdc069f19 Set copyright header to 2018 2018-01-28 13:16:20 -05:00
npezza93 3313716c87 Add new lines back in types 2018-01-28 13:15:42 -05:00
npezza93 735ffdd2ce Merge remote-tracking branch 'upstream/master' into alt-click 2018-01-28 13:13:37 -05:00
Daniel Imms 8588199fdb Merge pull request #1256 from Tyriar/1255_id_class
Change render layer id to a class
2018-01-28 10:13:13 -08:00
npezza93 f726c1fd0d Allow selection and alt click to live conflict free 2018-01-28 13:06:54 -05:00
npezza93 e60704255f Fix lints 2018-01-28 13:00:11 -05:00
npezza93 aeae40e110 Use the terminal and circular list interface 2018-01-28 12:38:20 -05:00
npezza93 08af2ebeea Utilize enum to simplify sequence generation 2018-01-28 12:29:52 -05:00
npezza93 0202659c38 Set terminal and mouseevent inside constructor args 2018-01-28 10:48:08 -05:00
npezza93 edadb6e0f1 Update the copyright header 2018-01-28 10:44:01 -05:00
Daniel Imms 39abf8aecf Change render layer id to a class
It's only used to identify them in DOM explorer currently

Fixes #1255
2018-01-27 22:26:00 -08:00
Daniel Imms 65b6901170 Fix compile error 2018-01-27 21:51:13 -08:00
npezza93 14725d0b0d Merge remote-tracking branch 'upstream/master' into alt-click 2018-01-27 22:38:12 -05:00
npezza93 c41a4b5168 Move repeat import 2018-01-27 22:37:57 -05:00
npezza93 d61aa6e470 Fix lints 2018-01-27 22:37:31 -05:00
npezza93 544fa67851 Move repeat out of generic 2018-01-27 22:36:53 -05:00
npezza93 6a48022a45 Don't reset starting position if the row is not moved.
Mainly due to the normal buffer
2018-01-27 22:28:41 -05:00
npezza93 a4ee723665 Fix backward row/col definition 2018-01-27 22:06:58 -05:00
npezza93 dc59f33ccf Fix translateBufferToString bug 2018-01-27 22:06:31 -05:00
Daniel Imms ec156f62e1 Only update a11y manager dimensions after a renderer resize 2018-01-27 18:38:28 -08:00
npezza93 833d814499 Refactor alt-click to work independently of which buffer is being used 2018-01-27 21:30:53 -05:00
npezza93 327b16ac2e Add repeat polyfill for repeating a string 2018-01-27 21:30:24 -05:00
Daniel Imms a3958a1cba Merge branch 'master' into 731_screen_reader 2018-01-27 18:15:31 -08:00
Daniel Imms 7c8b64aaf1 Revert "Hook up scroll APIs to scroll and focus when navigating"
This reverts commit b13826441c.
2018-01-27 18:14:58 -08:00
Daniel Imms b13826441c Hook up scroll APIs to scroll and focus when navigating 2018-01-27 18:14:52 -08:00
Daniel Imms 0bdf1e8265 Remove unused function 2018-01-27 17:51:43 -08:00
Daniel Imms 5506af5322 Clean up a11y manager 2018-01-27 16:47:23 -08:00
Daniel Imms 6439aca1d8 Refresh dimensions of only a single row 2018-01-27 15:00:19 -08:00
Daniel Imms 65850c1c20 Reduce duplication 2018-01-27 14:53:46 -08:00
Daniel Imms 963db4d2fd Remove navigation mode/more rows element 2018-01-27 14:48:45 -08:00
Daniel Imms 5c061195c0 Get navigation right/down working with nav mode 2018-01-27 14:45:22 -08:00
Saad Malik 0104e4214e Reimplement using map
* Reimplemented reg + shift alt digits and characters using a map
* Add test cases for digits
2018-01-26 18:33:37 -08:00
Saad Malik baaf61b3a3 Merge branch 'master' into ctrl-alt-sequences 2018-01-26 13:38:48 -08:00
Daniel Imms 9feb10efc5 Merge pull request #1244 from Tyriar/remove_legacy_gitignore
Remove legacy .gitignore rules
2018-01-26 10:34:43 -08:00
Daniel Imms 58f3793cd4 Merge branch 'master' into remove_legacy_gitignore 2018-01-26 10:27:44 -08:00
Daniel Imms 93715c455d Add dist/ back 2018-01-26 10:26:31 -08:00
Daniel Imms b9976c3d17 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-26 10:24:09 -08:00
sitzmar 069b489fb6 adjust timeout to ensure right-click event has fired 2018-01-26 08:04:59 -08:00
CHaBou fe53e1a091 Change canvas transparency when option is updated 2018-01-25 23:20:20 +01:00
Bruno Ribeito e6c6e0766a Fix usage of allowWhitespaceOnlySelection variable 2018-01-25 21:00:10 +00:00
Daniel Imms 2c065b66be Add SQL Operations Studio to real world uses 2018-01-25 12:31:48 -08:00
Daniel Imms 1dce76dbbb Add curly to tslint, fix errors
Fixes #1248
2018-01-25 12:17:59 -08:00
Daniel Imms 9c62a817d0 Fix build after merge 2018-01-25 12:08:50 -08:00
Daniel Imms 3a3f3db56e Merge remote-tracking branch 'ups/master' into feature-778 2018-01-25 11:58:46 -08:00
Daniel Imms bd55ce841e Merge branch 'master' into 1245_revert_page_change 2018-01-25 11:28:29 -08:00
Daniel Imms 6c54c6cded Merge pull request #1224 from Tyriar/1221_iterminal_typings
Move ITerminal and all its dependents into typings/
2018-01-25 11:25:02 -08:00
Daniel Imms c12394a687 Don't export contains 2018-01-25 11:09:20 -08:00
Daniel Imms 2e23dd34e0 Move xterm-internal types back into Types.ts files 2018-01-25 11:06:52 -08:00
Daniel Imms 8225b94d34 Types.d.ts -> .ts 2018-01-25 10:24:17 -08:00
Daniel Imms 4db4fcb40a Remove declaration file generation, we have explicit types 2018-01-25 10:23:14 -08:00
Daniel Imms 177edfa66e Merge interfaces and types files 2018-01-25 10:03:28 -08:00
Daniel Imms d454a50a84 Move FontWeight type to xterm.d.ts 2018-01-25 09:48:12 -08:00
Daniel Imms 7358890a31 Merge remote-tracking branch 'ups/master' into 1221_iterminal_typings 2018-01-25 09:45:13 -08:00
Daniel Imms 4c6f18c36f Revert "Add support for modifiers for PageUp / PageDown keys."
This reverts commit 83921c0b26.

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

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

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

Signed-off-by: Jianhui Zhao <jianhuizhao329@gmail.com>
2018-01-19 10:57:51 +08:00
Andres Mejia 5133f25fb7 Fix issue with resetting terminal.
Ensure new BufferSet is used and clear selection manager on reset.
2018-01-18 17:48:29 -05:00
Daniel Imms cfd00529eb Various fixes
- Add buffers being retained on full reset (CIS)
- Remove setup event, simplify
- Only have one trim listener active at once
- Disallow activate events if nothing changed (just in case)
2018-01-18 17:48:29 -05:00
Andres Mejia a9b7ef5dc3 Clear selection manager when the terminal switches its buffer set or buffer. 2018-01-18 17:48:29 -05:00
Andres Mejia 90c8c85c09 Fix issue with resizing terminal.
Need to ensure the active buffer is always being used.
Fixes https://github.com/xtermjs/xterm.js/issues/824 .
2018-01-18 17:48:28 -05:00
Bruno Ribeiro 9f8098c55b Merge branch 'master' into feature-778 2018-01-18 21:10:50 +00:00
Bruno Ribeiro b8f2832ed4 Merge branch 'master' into 1117 2018-01-18 21:10:09 +00:00
Daniel Imms 2f5029205d Merge pull request #1214 from Tyriar/fix_lint
Fix lint, update tslint and add more tslint rules
2018-01-18 12:28:02 -08:00
Daniel Imms 0645fe582f Fix missing space 2018-01-18 12:27:07 -08:00
Daniel Imms 9c4ef2fd86 Merge remote-tracking branch 'origin/master' into fix_lint 2018-01-18 12:11:21 -08:00
Daniel Imms 8d5ed6fc75 Merge pull request #1197 from blink1073/fix-typings
Ship the declaration files
2018-01-18 12:03:33 -08:00
Daniel Imms 1b48f2cd6d Merge branch 'master' into fix-typings 2018-01-18 11:56:10 -08:00
Daniel Imms 15dd1e6bec Merge pull request #1217 from Tyriar/1216_invalidate_cursor
Fix cursor lagging behind when blink is on
2018-01-18 10:54:26 -08:00
Daniel Imms b4b4456225 Merge branch 'master' into 1216_invalidate_cursor 2018-01-18 10:54:18 -08:00
Daniel Imms b89f9666e6 Merge pull request #1218 from Tyriar/travis_osx
Remove OSX from travis
2018-01-18 10:54:02 -08:00
Daniel Imms 2253e666e7 Remove OSX from travis
Builds take too long on free tier
2018-01-18 09:39:32 -08:00
Daniel Imms 777603b5cd Merge branch 'master' into 1216_invalidate_cursor 2018-01-18 09:01:27 -08:00
Daniel Imms 0379ef7bc7 Merge pull request #1215 from markus-oberhumer/ctrl-pageup-pagedown
Add support for modifiers for PageUp / PageDown keys.
2018-01-18 07:58:31 -08:00
Daniel Imms b1beee3595 Fix cursor lagging behind when blink is on
Fixes #1216
2018-01-18 07:54:45 -08:00
Markus F.X.J. Oberhumer 83921c0b26 Add support for modifiers for PageUp / PageDown keys.
This allows using Ctrl-PageUp/Down and Alt-PageUp/Down.
2018-01-18 09:23:05 +01:00
Daniel Imms 0f04f9fbb4 Rotate rows to ensure an item will be read in nav mode when scrolling 2018-01-17 14:10:03 -08:00
Daniel Imms abfa58fca9 Keep track of nav mode focused element 2018-01-17 10:52:12 -08:00
Daniel Imms 4588d15738 Add more tslint rules, fix attach addon 2018-01-17 10:40:42 -08:00
Daniel Imms 307dd0fd68 Add several tslint rules 2018-01-17 10:32:35 -08:00
Daniel Imms 53e1f83bb5 Update tslint, fix lint 2018-01-17 10:23:27 -08:00
Daniel Imms 70236f6818 Fix addon lint 2018-01-17 10:20:04 -08:00
Daniel Imms 78d734117b Support aria posinset and setsize 2018-01-17 09:56:57 -08:00
Daniel Imms f48da12c16 Prevent event propagation when handled by nav mode 2018-01-16 11:24:19 -08:00
Daniel Imms 3fa4b4941f Support nav mode pgup/pgdown/end/home, check boundaries 2018-01-16 11:01:45 -08:00
Daniel Imms 222c42f7ea Expose screen reader and navigation mode on demo/API 2018-01-16 10:29:40 -08:00
Daniel Imms 34eb08952c Fix RenderDebouncer not refreshing whole viewport when asked 2018-01-16 10:26:09 -08:00
Daniel Imms e9b55930d8 Fix test mocks 2018-01-16 10:12:37 -08:00
Daniel Imms 1e9fb8696a Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-16 09:56:19 -08:00
Daniel Imms 5888cd7938 Merge pull request #1210 from Tyriar/1194_initial_cursorBlink
Ensure renderer uses latest options at open
2018-01-16 09:21:39 -08:00
Daniel Imms 0e870d106f Fix search for emojis
Fixes #1103
2018-01-16 07:26:03 -08:00
Daniel Imms 8d3bdad7e4 Fix search for wide unicode
Part of #1103
2018-01-16 07:06:10 -08:00
Daniel Imms f02ead309b Merge branch 'master' into 1194_initial_cursorBlink 2018-01-16 06:45:58 -08:00
Daniel Imms 80f13792a3 Ensure renderer uses latest options at open
Fixes #1194
2018-01-16 06:34:22 -08:00
Paris Kasidiaris 691f8a7567 Merge pull request #1199 from ficristo/umd-addons
Make addons UMD compatible
2018-01-15 22:11:21 +02:00
Daniel Imms 81b4d8fe9c Dispose of NavigationMode when AccessibilityManager is 2018-01-14 13:42:36 -08:00
Daniel Imms 23bd60a733 Properly dispose of disposables 2018-01-14 13:28:43 -08:00
Daniel Imms 85c02f10bf Improve dispoable listener functions 2018-01-14 13:18:00 -08:00
Daniel Imms 70489e5035 Move navigation mode stuff into its own class 2018-01-14 12:59:57 -08:00
ficristo 46f684621c Merge branch 'master' into umd-addons 2018-01-14 20:24:52 +01:00
Daniel Imms adcbb1ae3f Merge pull request #1207 from amejia1/new-use-case
Add use case for atom-xterm.
2018-01-13 20:55:41 -08:00
Andres Mejia a7101b5bcb Add use case for atom-xterm. 2018-01-13 15:34:05 -05:00
Daniel Imms 1e41b11319 Remove logs 2018-01-12 21:15:53 -08:00
Daniel Imms cc80114748 Merge pull request #1205 from jluk/patch-1
Include cloud shell in xterm applications
2018-01-12 17:41:08 -08:00
Justin Luk 5a147f201f Include cloud shell in xterm applications 2018-01-12 15:13:19 -08:00
Daniel Imms 2a187a26e0 Update a11y rows on scroll 2018-01-11 23:15:02 -08:00
Daniel Imms 2311d7d66b Add basic navigation mode support for current viewport 2018-01-11 22:56:19 -08:00
Daniel Imms bbc9a010b9 Listen to dpr change in a11y manager 2018-01-11 09:27:37 -08:00
Bruno Ribeito b75f52734d Use Web audio API 2018-01-08 20:53:26 +00:00
Daniel Imms a19b05f813 Fix bad type check that caused rows to not refresh 2018-01-08 09:53:06 -08:00
ficristo b7c0ca11fe Make addons UMD compatible 2018-01-07 09:02:41 +01:00
Daniel Imms e49d844284 Add note in README about code structure 2018-01-06 13:46:27 -08:00
Daniel Imms d0ef868215 Refactor char atlas to enable generation with worker
This doesn't actually add workers but it separates the code nicely
such that the minimal amount of relevant code can be pulled in by
worker code.

Part of #955
2018-01-06 13:43:34 -08:00
Steven Silvester a36a17928d Ship the declaration files 2018-01-06 07:18:07 -06:00
Daniel Imms 50ed83ca64 Set version to 3.1.0-master
This will make it clear in bug reports etc. when someone is running off
master and not 3.0.
2018-01-05 14:40:41 -08:00
Daniel Imms c46304c716 Merge remote-tracking branch 'origin/master' into 731_screen_reader 2018-01-05 14:28:08 -08:00
Daniel Imms be18507df6 Merge pull request #1172 from Tyriar/1118_dpi_change
Update charatlas when monitor DPI changes
2018-01-05 14:24:12 -08:00
Daniel Imms f6da87f3b4 Merge remote-tracking branch 'origin/master' into 1118_dpi_change 2018-01-05 14:19:21 -08:00
Daniel Imms 248fbccfd9 Remove console.log 2018-01-05 14:18:16 -08:00
Daniel Imms eac14a149f Merge pull request #1144 from Tyriar/880_renderer_pause
Use IntersectionObserver to turn off renderer when hidden
2018-01-05 14:16:10 -08:00
Daniel Imms d90f48b210 Merge branch 'master' into 880_renderer_pause 2018-01-05 14:15:26 -08:00
Daniel Imms eb1110e3f5 Merge pull request #1191 from astefanutti/kubebox
Add Kubebox to real-world uses
2018-01-05 13:35:59 -08:00
Antonin Stefanutti 82e7fe5a1b Add Kubebox to real-world uses 2018-01-05 22:12:32 +01:00
Daniel Imms 1bf084921e Merge branch 'master' into 1117 2018-01-05 11:03:50 -08:00
Daniel Imms 25972d1e06 Merge branch 'master' into feature-778 2018-01-05 11:03:33 -08:00
Daniel Imms b3c8923709 Merge branch 'master' into 1117 2018-01-05 11:03:24 -08:00
Daniel Imms 13d1020308 Merge pull request #1137 from damz/pr/parser-escape-state
Fix parser not resetting state for some escape sequences
2018-01-05 11:02:24 -08:00
Daniel Imms b98f10625f Merge branch 'master' into pr/parser-escape-state 2018-01-05 11:02:14 -08:00
Daniel Imms 09605dba16 Merge pull request #1142 from Tyriar/1067_fix_cursor_on_right
Fix cursor on scroll bar
2018-01-05 11:01:30 -08:00
Daniel Imms 1509f88dbf Merge branch 'master' into 1067_fix_cursor_on_right 2018-01-05 11:01:21 -08:00
Daniel Imms 884c7c631d Merge branch 'master' into 880_renderer_pause 2018-01-05 11:01:06 -08:00
Daniel Imms b66578a4ca Merge branch 'master' into 880_renderer_pause 2018-01-05 11:00:11 -08:00
Daniel Imms b0b6c8d878 Merge branch 'master' into transparency 2018-01-05 10:58:59 -08:00
Daniel Imms 9f1f86c316 Merge pull request #1168 from Tyriar/1152_npe_selection_manager_2
Prevent dragging from setting selection y over bounds
2018-01-05 10:58:40 -08:00
Daniel Imms 80a1c2bd50 Merge branch 'master' into 1152_npe_selection_manager_2 2018-01-05 10:58:22 -08:00
Daniel Imms 09139cc350 Merge pull request #1177 from electerm/add-electerm-use-case
add Real-world use: electerm
2018-01-05 10:56:08 -08:00
Daniel Imms 019eb30b68 Merge pull request #1180 from Tyriar/1179_alt_backspace
Allow alt+backspace to delete a word
2018-01-05 10:55:57 -08:00
Daniel Imms 58f80c404f Merge branch 'master' into add-electerm-use-case 2018-01-05 09:14:37 -08:00
Daniel Imms 22f860536d Merge branch 'master' into add-electerm-use-case 2018-01-05 09:13:55 -08:00
Daniel Imms e9ec1bda3d Merge branch 'master' into 1179_alt_backspace 2018-01-05 09:13:32 -08:00
Daniel Imms 9a743ef301 Merge pull request #1188 from xtermjs/Tyriar-patch-1
Remove repo redirect from package.json
2018-01-05 09:12:44 -08:00
Daniel Imms 0de2d3846a Merge branch 'master' into Tyriar-patch-1 2018-01-05 09:12:30 -08:00
Daniel Imms 0dd9b8d025 Remove repo redirect from package.json 2018-01-05 08:20:40 -08:00
Daniel Imms e20d2caa0e Use consistent refresh/render method names 2018-01-05 08:19:30 -08:00
Daniel Imms a846e8c08d Pull render animation frame logic into a helper class 2018-01-05 08:15:21 -08:00
Daniel Imms c332aeb51f Remove obsolete code and resolved TODOs 2018-01-05 07:51:22 -08:00
Daniel Imms bb830eb1ae Rate limit row refresh, tweak live region 2018-01-05 07:39:45 -08:00
Daniel Imms 6d1064fc01 Set height of a11y rows on creation 2018-01-03 10:08:24 -08:00
Daniel Imms 5ca15cfe3b Fix linefeed event
This was causing new lines to join words together
2018-01-03 09:54:06 -08:00
Daniel Imms 17964a517c Add screenReaderMode to typings 2018-01-03 09:28:24 -08:00
Daniel Imms 8cb7a9dabe Merge remote-tracking branch 'origin/v3' into 731_screen_reader 2018-01-03 09:09:42 -08:00
Daniel Imms 0819759814 Allow alt+backspace to delete a word
Fixes #1179
2018-01-02 10:33:17 -08:00
Daniel Imms ecc1055c10 Listen to resolution media query 2018-01-02 10:11:24 -08:00
ZHAO Xudong ac1023dac6 add Real-world use: electerm 2018-01-02 14:45:43 +08:00
Daniel Imms 72d946e089 Use a more robust solution to changing devicePixelRatio 2017-12-29 13:54:01 -08:00
Daniel Imms ef5beb6706 Update charatlas when monitor DPI changes
Fixes #1118
2017-12-29 08:30:13 -08:00
Daniel Imms cf2116ff6a Prevent dragging from setting selection y over bounds
Fixes #1152
2017-12-27 08:08:28 -08:00
Daniel Imms 5dc57a1305 Add detach logic back for mac only 2017-12-20 11:06:32 -08:00
Daniel Imms 0d65cb3f69 Add max rows to read to handle spam case 2017-12-19 16:27:03 -08:00
Daniel Imms ae957ecb3e Get output reading working on Windows, refresh on window resize 2017-12-19 15:48:58 -08:00
Daniel Imms d03f5b0c2a Range of improvements for macOS VoiceOver 2017-12-19 15:16:28 -08:00
Daniel Imms e5d57b08c8 Merge remote-tracking branch 'ups/v3' into 731_screen_reader 2017-12-19 13:28:31 -08:00
Daniel Imms 399779692d Initial prototype 2017-12-19 12:57:58 -08:00
CHaBou 9c5aaed441 Fix some function parameters 2017-12-15 00:02:37 +01:00
CHaBou 9f85790ad1 Add allowTransparency option
Fixes #1004
2017-12-14 22:55:01 +01:00
Daniel Imms 47a50acc55 Use IntersectionObserver to turn off renderer when hidden
Fixes #880
2017-12-11 15:16:13 -08:00
Daniel Imms 616f0f9d36 Fix cursor on scroll bar
Fixes #1067
2017-12-11 10:13:17 -08:00
Damien Tournoud 981da3d9e6 Fix parser not resetting state for some escape sequences 2017-12-02 20:45:23 -08:00
Bruno Ribeito 6822070222 Fix typo + Cleanup logic 2017-11-12 23:15:09 +00:00
Bruno Ribeito 51de968fde Fix non-white colors not being drawn as bold 2017-11-12 23:05:27 +00:00
Bruno Ribeito f12eb5367b Adress feedback 2017-11-12 22:04:10 +00:00
Bruno Ribeito 007410995a Fix#1117 2017-11-12 22:04:10 +00:00
Bruno Ribeito 18b86a1cbc Do the selection even if there is already a selection 2017-11-08 23:27:00 +00:00
Bruno Ribeito 2865d935b6 Adress feedback 2017-11-07 21:21:44 +00:00
Bruno Ribeito 1877e7019d Implement #778 2017-11-06 23:20:57 +00:00
124 changed files with 5679 additions and 15373 deletions
+2 -5
View File
@@ -15,14 +15,11 @@ build/
.vscode/
.DS_Store
fixtures/typings-test/*.js
package-lock.json
# Directories needed for code coverage
/coverage/
# Keep legacy files out of the repo, this can be removed when we merge v3 into master
# Keep bundled code out of Git
dist/
src/utils/TestUtils.ts
src/xterm.js
# Keep the demo builds out of Git
demo/dist/
+44 -12
View File
@@ -1,16 +1,48 @@
node_modules/
*.swp
.lock-wscript
lib/*.test.js
lib/*.test.js.map
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
*
!*/
# Whitelist - entries to be included must be negated with "!"
!*.js
!*.json
# Whitelist - dist/
!dist/**/*.js
!dist/**/*.js.map
!dist/**/*.css
# Whitelist - lib/
!lib/**/*.d.ts
!lib/**/*.js
!lib/**/*.js.map
!lib/**/*.css
# Whitelist - src/
!src/**/*.ts
!src/**/*.d.ts
!src/**/*.js
!src/**/*.js.map
!src/**/*.css
# Whitelist - typings/
!typings/*.d.ts
# Blacklist - (normal behavior) these will override any whitelist
*.test.ts
*.test.d.ts
*.test.js
*.test.js.map
lib/test/
Makefile.gyp
*.Makefile
*.target.gyp.mk
*.node
example/*.log
docs/
npm-debug.log
/.idea/
.env
.vscode/
build/
fixtures/
coverage/
demo/
+1
View File
@@ -0,0 +1 @@
package-lock=false
+1 -2
View File
@@ -1,7 +1,6 @@
language: node_js
os:
- linux
- osx
node_js:
- 6
before_install:
@@ -12,9 +11,9 @@ before_install:
- npm install -g npm@5.1.0
env:
matrix:
- NPM_COMMAND=tsc
- NPM_COMMAND=lint
- NPM_COMMAND=test
notifications:
email: false
script: npm run $NPM_COMMAND
after_success: npm run coveralls
+28
View File
@@ -1,12 +1,16 @@
List of xterm.js contributors. Updated before every release.
Adrian Labbé <adrilabbelol@gmail.com>
Aleksandr Andrienko <aandrienko@codenvy.com>
Aleksandr Andriienko <oandriie@redhat.com>
Alessandro Nadalin <alessandro.nadalin@gmail.com>
Alexander Olsson <noseglid@gmail.com>
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
Alexey Kontsevoy <alexey@gravitational.com>
Andres Mejia <amejia004@gmail.com>
Anish Athalye <me@anishathalye.com>
Anthony Lapenna <lapenna.anthony@gmail.com>
Antonin Stefanutti <antonin@stefanutti.fr>
Antonis Kalipetis <akalipetis@sourcelair.com>
Anton Skshidlevsky <meefik@gmail.com>
Anton Yurovskykh <anton.yurovskykh@gmail.com>
@@ -15,10 +19,13 @@ Austin Robertson <austinrobertson@gmail.com>
ayapi <colors.aya@gmail.com>
Ben Hall <ben@benhall.me.uk>
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
Benjamin Woodruff <github@benjam.info>
Bill Church <billchurch@users.noreply.github.com>
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
bottleofwater <nison.mael+bottleofwater@gmail.com>
Brandon Bayer <b@bayer.ws>
Brian Mock <brian@mockbrian.com>
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
Carson Anderson <carson@betterservers.com>
CHaBou <chabup@delean.fr>
@@ -26,7 +33,9 @@ Christian Budde Christensen <budde377@gmail.com>
Christof Marti <chrmarti@microsoft.com>
Christopher Jeffrey <chjjeffrey@gmail.com>
coderaiser <mnemonic.enemy@gmail.com>
Damien Tournoud <damien@platform.sh>
Dan Brown <jdanbrown@gmail.com>
Daniel Griffen <dagriffe@microsoft.com>
Daniel Griffen <daniel@griffen.io>
Daniel Imms <daimms@microsoft.com>
Daniel Risacher <drisacher@gmail.com>
@@ -38,6 +47,7 @@ Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
Elliot Saba <staticfloat@gmail.com>
Exile <exile@chamber.ee>
Felipe Gasper <felipe@felipegasper.com>
Felix <30559812+felixse@users.noreply.github.com>
ficristo <ficristo.work@gmail.com>
Gary Ritchie <gary@rstudio.com>
hiro-su <h.sugipon@gmail.com>
@@ -46,24 +56,35 @@ imoses <ido@twiggle.com>
InDieTasten <indietasten@gmail.com>
irokas <akasidiari@inf.uth.gr>
Jakob Gillich <jakob@gillich.me>
Jan Kuri <jkuri88@gmail.com>
Jean Bruenn <himself@jeanbruenn.info>
Jeremy Danyow <jedanyow@microsoft.com>
Jianhui Zhao <jianhuizhao329@gmail.com>
Joao Moreno <jomo@microsoft.com>
Joao Moreno <mail@joaomoreno.com>
Johannes Zellner <johannes@nebulon.de>
Jon Austin <jon.i.austin@gmail.com>
Jon Masters <jon.masters@sky.com>
Jörg Breitbart <jerch@rockborn.de>
jpoth <poth.john@gmail.com>
Justin Luk <jluk@users.noreply.github.com>
Justin Mecham <justin@mecham.me>
Kirill Merkushev <lanwen@yandex.ru>
Krasimir Tsonev <krasimir@outset.ws>
Ledion Bitincka <lbitincka@gmail.com>
Linus Unnebäck <linus@folkdatorn.se>
Luca <LucaT1@users.noreply.github.com>
Lucian Buzzo <lucian.buzzo@gmail.com>
Lukas Drgon <lukas.drgon@gmail.com>
Maël Nison <nison.mael@gmail.com>
Marc Dumais <marc.dumais@ericsson.com>
Marek Libra <mlibra@redhat.com>
Markus F.X.J. Oberhumer <markus@oberhumer.com>
Martin Chloride <i@martincl2.me>
Martin Koppehel <martin.koppehel@st.ovgu.de>
Martin Wang <jiahaow@ca.ibm.com>
Matt Bierner <matb@microsoft.com>
Matthew James <matthew.d.james87@gmail.com>
Michael Irwin <mikesir87@gmail.com>
Mikko Karvonen <mikko.karvonen@arm.com>
mofux <t.zilz@mofux.org>
@@ -72,13 +93,19 @@ npezza93 <npezza93@gmail.com>
Oleksandr Andriienko <oandriie@redhat.com>
Paris Kasidiaris <pariskasidiaris@gmail.com>
Paris Kasidiaris <paris@sourcelair.com>
Peng Xiao <pengxiao@outlook.com>
Peter Baumgarten <me@peterbaumgarten.com>
Philip Olson <philip.olson@protonmail.ch>
pro-src <34285059+pro-src@users.noreply.github.com>
pro-src <rodneyd.teal@gmail.com>
Rick Baker <rick@ricktbaker.com>
runarberg <runar@greenqloud.com>
Saad Malik <simfox3@gmail.com>
Samuel Williams <samuel.williams@oriontransfer.co.nz>
Saswat Das <saswatds@users.noreply.github.com>
Saul Costa <saul@codevolve.com>
Shuanglei Tao <tsl0922@gmail.com>
sitzmar <sitzmar@gmail.com>
Steven Silvester <steven.silvester@ieee.org>
stuicey <stuicey@users.noreply.github.com>
t-amqi <umberos@gmail.com>
@@ -90,3 +117,4 @@ Tyler Jewell <tjewell@codenvy.com>
Vincent Woo <me@vincentwoo.com>
yutaka <shigure@refy.net>
YuviPanda <yuvipanda@gmail.com>
ZHAO Xudong <zxdong@gmail.com>
+4
View File
@@ -50,3 +50,7 @@ By contributing code to xterm.js you
holder has explicitly granted the right to use it like this,
through a compatible open source license or through a direct
agreement with you.)
### Third party dependencies
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
+2 -2
View File
@@ -1,4 +1,4 @@
FROM node:6
FROM node:8
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
# Set the working directory
@@ -16,4 +16,4 @@ RUN npm install
COPY . /usr/src/app
# Run the tests and build, to make sure everything is working nicely
RUN npm run build && npm run test
RUN npm run build && npm run webpack && npm run test
+1
View File
@@ -1 +1,2 @@
web: npm start
webpack: npm run watch
+94 -14
View File
@@ -1,6 +1,6 @@
# [![xterm.js logo](logo-full.png)](https://xtermjs.org)
[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/sourcelair/xterm.js/badge.svg)](https://coveralls.io/github/sourcelair/xterm.js) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
[![xterm.js build status](https://api.travis-ci.org/xtermjs/xterm.js.svg)](https://travis-ci.org/xtermjs/xterm.js) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
@@ -24,7 +24,7 @@ It enables applications to provide fully featured terminals to their users and c
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
```
npm install
npm install xterm
```
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your html page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself.
@@ -41,7 +41,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
<script>
var term = new Terminal();
term.open(document.getElementById('terminal'));
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
</script>
</body>
</html>
@@ -57,8 +57,6 @@ The proposed way to load xterm.js is via the ES6 module syntax.
import { Terminal } from 'xterm';
```
*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.*
### Addons
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
@@ -76,6 +74,27 @@ var xterm = new Terminal(); // Instantiate the terminal
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
```
#### Importing Addons in TypeScript
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
```typescript
import { Terminal } from 'xterm';
import { fit } from 'xterm/lib/addons/fit/fit';
const xterm = new Terminal();
// Fit the terminal when necessary:
fit(xterm);
```
#### Third party addons
There are also the following third party addons available:
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
## Browser Support
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
@@ -110,7 +129,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
@@ -118,32 +137,89 @@ computational environment for Jupyter, supporting interactive data science and s
- [**Script Runner**](https://github.com/ioquatix/script-runner): Run scripts (or a shell) in Atom.
- [**Whack Whack Terminal**](https://github.com/Microsoft/WhackWhackTerminal): Terminal emulator for Visual Studio 2017.
- [**VTerm**](https://github.com/vterm/vterm): Extensible terminal emulator based on Electron and React.
- [**electerm**](http://electerm.html5beta.com): electerm is a terminal/ssh/sftp client(mac, win, linux) based on electron/node-pty/xterm.
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
- [**Azure Cloud Shell**](https://shell.azure.com): Azure Cloud Shell is a Microsoft-managed admin machine built on Azure, for Azure.
- [**atom-xterm**](https://atom.io/packages/atom-xterm): Atom plugin for providing terminals inside your Atom workspace.
- [**rtty**](https://github.com/zhaojh329/rtty): A reverse proxy WebTTY. It is composed of the client and the server.
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
- [**Hyper**](https://hyper.is): A terminal built on web technologies
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
## Demo
### Linux or macOS
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run these commands:
Below you can find instructions on how to run the demo on different platforms.
### SourceLair
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
### Docker
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
To run the just demo (with no editing access). run the following command in your terminal:
```
docker run -p 3000:3000 xtermjs/xterm.js
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
To run the demo and builder in parallel, run the following command in your terminal:
```
docker-compose up
```
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
### Foreman (or other Procfile runner)
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
```
Then open http://0.0.0.0:3000 in a web browser.
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Linux or macOS
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
```
npm install
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://0.0.0.0:3000 in a web browser to access the demo.
### Windows
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run these commands.
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
```
npm install
npm start
npm start # Run this in its own terminal
npm run watch # Run this in its own terminal
```
Then open http://127.0.0.1:3000 in a web browser.
Then open http://127.0.0.1:3000 in a web browser to access the demo.
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
@@ -159,10 +235,14 @@ Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few ex
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
### Code structure
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker.
## License Agreement
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+27 -2
View File
@@ -9,7 +9,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
</head>
<body>
<h1>xterm.js: xterm, in the browser</h1>
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
<div id="terminal-container"></div>
<div>
<h2>Actions</h2>
@@ -23,6 +23,12 @@
<p>
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
</p>
<p>
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
</p>
<p>
<label><input type="checkbox" id="option-transparency"> transparency</label>
</p>
<p>
<label>
cursorStyle
@@ -50,6 +56,16 @@
<p>
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
</p>
<p>
<label>
experimentalCharAtlas
<select id="option-experimental-char-atlas">
<option value="static" selected>static</option>
<option value="dynamic">dynamic</option>
<option value="none">none</option>
</select>
</label>
</p>
<div>
<h3>Size</h3>
<div>
@@ -61,10 +77,19 @@
<label for="rows">Rows</label>
<input type="number" id="rows" />
</div>
<div style="display: inline-block; margin-right: 16px;">
<label for="padding">Padding</label>
<input type="number" id="padding" />
</div>
</div>
</div>
<div>
<h3>Accessibility</h3>
<p>
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
</p>
</div>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
<script src="dist/bundle.js" defer ></script>
</body>
</html>
+36 -8
View File
@@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach';
import * as fit from '../build/addons/fit/fit';
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
import * as search from '../build/addons/search/search';
import * as webLinks from '../build/addons/webLinks/webLinks';
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
Terminal.applyAddon(fullscreen);
Terminal.applyAddon(search);
Terminal.applyAddon(webLinks);
Terminal.applyAddon(winptyCompat);
@@ -27,28 +29,36 @@ var terminalContainer = document.getElementById('terminal-container'),
optionElements = {
cursorBlink: document.querySelector('#option-cursor-blink'),
cursorStyle: document.querySelector('#option-cursor-style'),
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
scrollback: document.querySelector('#option-scrollback'),
transparency: document.querySelector('#option-transparency'),
tabstopwidth: document.querySelector('#option-tabstopwidth'),
bellStyle: document.querySelector('#option-bell-style')
experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'),
bellStyle: document.querySelector('#option-bell-style'),
screenReaderMode: document.querySelector('#option-screen-reader-mode')
},
colsElement = document.getElementById('cols'),
rowsElement = document.getElementById('rows');
rowsElement = document.getElementById('rows'),
paddingElement = document.getElementById('padding');
function setTerminalSize() {
var cols = parseInt(colsElement.value, 10);
var rows = parseInt(rowsElement.value, 10);
var viewportElement = document.querySelector('.xterm-viewport');
var scrollBarWidth = viewportElement.offsetWidth - viewportElement.clientWidth;
var width = (cols * term.renderer.dimensions.actualCellWidth + 20 /*room for scrollbar*/).toString() + 'px';
var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px';
var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px';
terminalContainer.style.width = width;
terminalContainer.style.height = height;
term.resize(cols, rows);
term.fit();
}
function setPadding() {
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
term.fit();
}
colsElement.addEventListener('change', setTerminalSize);
rowsElement.addEventListener('change', setTerminalSize);
paddingElement.addEventListener('change', setPadding);
actionElements.findNext.addEventListener('keypress', function (e) {
if (e.key === "Enter") {
@@ -66,6 +76,14 @@ actionElements.findPrevious.addEventListener('keypress', function (e) {
optionElements.cursorBlink.addEventListener('change', function () {
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
});
optionElements.macOptionIsMeta.addEventListener('change', function () {
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
});
optionElements.transparency.addEventListener('change', function () {
var checked = optionElements.transparency.checked;
term.setOption('allowTransparency', checked);
term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {});
});
optionElements.cursorStyle.addEventListener('change', function () {
term.setOption('cursorStyle', optionElements.cursorStyle.value);
});
@@ -78,6 +96,12 @@ optionElements.scrollback.addEventListener('change', function () {
optionElements.tabstopwidth.addEventListener('change', function () {
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
});
optionElements.experimentalCharAtlas.addEventListener('change', function () {
term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value);
});
optionElements.screenReaderMode.addEventListener('change', function () {
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
});
createTerminal();
@@ -87,9 +111,11 @@ function createTerminal() {
terminalContainer.removeChild(terminalContainer.children[0]);
}
term = new Terminal({
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
cursorBlink: optionElements.cursorBlink.checked,
scrollback: parseInt(optionElements.scrollback.value, 10),
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10),
screenReaderMode: optionElements.screenReaderMode.checked
});
window.term = term; // Expose `term` to window for debugging purposes
term.on('resize', function (size) {
@@ -107,6 +133,7 @@ function createTerminal() {
term.open(terminalContainer);
term.winptyCompatInit();
term.webLinksInit();
term.fit();
term.focus();
@@ -114,6 +141,7 @@ function createTerminal() {
setTimeout(function () {
colsElement.value = term.cols;
rowsElement.value = term.rows;
paddingElement.value = 0;
// Set terminal size again to set the specific dimensions on the demo
setTerminalSize();
@@ -7,16 +7,16 @@ var pty = require('node-pty');
var terminals = {},
logs = {};
app.use('/build', express.static(__dirname + '/../../../../build'));
app.use('/demo', express.static(__dirname + '/../../../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../../../node_modules/zmodem.js/dist'));
app.use('/build', express.static(__dirname + '/../../build'));
app.use('/demo', express.static(__dirname + '/../../demo'));
app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist'));
app.get('/', function(req, res){
res.sendFile(__dirname + '/index.html');
});
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
res.sendFile(__dirname + '../style.css');
});
app.get('/main.js', function(req, res){
@@ -6,6 +6,11 @@ var term,
socket,
pid;
Terminal.applyAddon(fit);
Terminal.applyAddon(attach);
Terminal.applyAddon(zmodem);
Terminal.applyAddon(search);
var terminalContainer = document.getElementById('terminal-container'),
actionElements = {
findNext: document.querySelector('#find-next'),
+8
View File
@@ -2,8 +2,16 @@ version: "3"
services:
web:
image: xtermjs/xterm.js:latest
build: .
volumes:
- ./:/usr/src/app
ports:
- ${XTERMJS_PORT:3000}:3000
command: ["npm", "start"]
watch:
build: .
volumes:
- ./:/usr/src/app
command: ["npm", "run", "watch"]
+12
View File
@@ -144,6 +144,11 @@ namespace methods_core {
const r20: string = t.getOption('bellStyle');
const r21: boolean = t.getOption('enableBold');
const r22: number = t.getOption('letterSpacing');
const r23: boolean = t.getOption('macOptionIsMeta');
const r24: string = t.getOption('fontWeight');
const r25: string = t.getOption('fontWeightBold');
const r26: boolean = t.getOption('allowTransparency');
const r27: boolean = t.getOption('rightClickSelectsWord');
}
{
const t: Terminal = new Terminal();
@@ -157,9 +162,14 @@ namespace methods_core {
t.setOption('debug', true);
t.setOption('disableStdin', true);
t.setOption('enableBold', true);
t.setOption('fontWeight', 'normal');
t.setOption('fontWeight', 'bold');
t.setOption('fontWeightBold', 'normal');
t.setOption('fontWeightBold', 'bold');
t.setOption('popOnBell', true);
t.setOption('screenKeys', true);
t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true);
t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1);
@@ -177,6 +187,8 @@ namespace methods_core {
t.setOption('lineHeight', 1);
t.setOption('fontFamily', 'foo');
t.setOption('theme', {background: '#ff0000'});
t.setOption('macOptionIsMeta', true);
t.setOption('rightClickSelectsWord', false);
}
}
namespace scrolling {
+42 -46
View File
@@ -18,11 +18,13 @@ const ts = require('gulp-typescript');
const util = require('gulp-util');
const webpack = require('webpack-stream');
let buildDir = process.env.BUILD_DIR || 'build';
let tsProject = ts.createProject('tsconfig.json');
let srcDir = tsProject.config.compilerOptions.rootDir;
const buildDir = process.env.BUILD_DIR || 'build';
const tsProject = ts.createProject('tsconfig.json');
const srcDir = tsProject.config.compilerOptions.rootDir;
let outDir = tsProject.config.compilerOptions.outDir;
const addons = fs.readdirSync(`${__dirname}/src/addons`);
// Under some environments like TravisCI, this comes out at absolute which can
// break the build. This ensures that the outDir is absolute.
if (path.normalize(outDir).indexOf(__dirname) !== 0) {
@@ -45,15 +47,19 @@ gulp.task('tsc', function () {
tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir))
);
let addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
let addonStreams = addons.map(function(addon) {
fs.emptyDirSync(`${outDir}/addons/${addon}`);
let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`);
let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon());
let tscAddon = tsResultAddon.js
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`));
let tscAddon = merge(
tsResultAddon.js
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`)),
tsResultAddon.dts
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
.pipe(gulp.dest(`${outDir}/addons/${addon}`))
)
return tscAddon;
});
@@ -103,45 +109,29 @@ gulp.task('browserify', ['tsc'], function() {
});
gulp.task('browserify-addons', ['tsc'], function() {
let searchOptions = {
basedir: `${buildDir}/addons/search`,
debug: true,
entries: [`${outDir}/addons/search/search.js`],
cache: {},
packageCache: {}
};
let searchBundle = browserify(searchOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/search/search.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
const bundles = addons.map((addon) => {
const addonOptions = {
basedir: `${buildDir}/addons/${addon}`,
debug: true,
entries: [`${outDir}/addons/${addon}/${addon}.js`],
standalone: addon,
cache: {},
packageCache: {}
};
let winptyCompatOptions = {
basedir: `${buildDir}/addons/winptyCompat`,
debug: true,
entries: [`${outDir}/addons/winptyCompat/winptyCompat.js`],
cache: {},
packageCache: {}
};
let winptyCompatBundle = browserify(winptyCompatOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source('./addons/winptyCompat/winptyCompat.js'))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
const addonBundle = browserify(addonOptions)
.external(path.join(outDir, 'Terminal.js'))
.bundle()
.pipe(source(`./addons/${addon}/${addon}.js`))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: ''}))
.pipe(sourcemaps.write('./'))
.pipe(gulp.dest(buildDir));
// Copy all add-ons from outDir to buildDir
let copyAddons = gulp.src([
// Copy JS addons
`${outDir}/addons/**/*`
]).pipe(gulp.dest(`${buildDir}/addons`));
return addonBundle;
});
return merge(searchBundle, winptyCompatBundle, copyAddons);
return merge(...bundles);
});
gulp.task('instrument-test', function () {
@@ -189,9 +179,11 @@ gulp.task('sorcery', ['browserify'], function () {
});
gulp.task('sorcery-addons', ['browserify-addons'], function () {
var chain = sorcery.loadSync(`${buildDir}/addons/search/search.js`);
chain.apply();
chain.writeSync();
addons.forEach((addon) => {
const chain = sorcery.loadSync(`${buildDir}/addons/${addon}/${addon}.js`);
chain.apply();
chain.writeSync();
})
});
gulp.task('webpack', ['build'], function() {
@@ -200,6 +192,10 @@ gulp.task('webpack', ['build'], function() {
.pipe(gulp.dest('demo/dist/'));
});
gulp.task('watch', ['webpack'], () => {
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
});
/**
* Submit coverage results to coveralls.io
*/
-26
View File
@@ -1,26 +0,0 @@
{
"source": {
"include": [
"src/"
],
"exclude": [
"src/test/"
],
"excludePattern": "src/.+\\.test\\.(js|ts)$"
},
"opts": {
"readme": "README.md",
"template": "node_modules/docdash",
"encoding": "utf8",
"destination": "docs/",
"recurse": true,
"verbose": true
},
"plugins": [
"plugins/markdown"
],
"templates": {
"cleverLinks": false,
"monospaceLinks": false
}
}
-12651
View File
File diff suppressed because it is too large Load Diff
+15 -46
View File
@@ -1,52 +1,19 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
"version": "3.0.0",
"ignore": [
"demo",
"test",
".gitignore"
],
"version": "3.4.0",
"main": "lib/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/sourcelair/xterm.js",
"repository": "https://github.com/xtermjs/xterm.js",
"license": "MIT",
"files": [
"*.js",
"*.json",
"dist/*.css",
"dist/**/*.css",
"dist/*.js",
"dist/*.js.map",
"dist/**/*.js",
"dist/**/*.js.map",
"lib/*.css",
"lib/**/*.css",
"lib/*.d.ts",
"lib/*.js",
"lib/*.js.map",
"lib/**/*.d.ts",
"lib/**/*.js",
"lib/**/*.js.map",
"src/*.css",
"src/**/*.css",
"src/*.js",
"src/*.js.map",
"src/*.ts",
"src/**/*.js",
"src/**/*.js.map",
"src/**/*.ts",
"typings/*.d.ts"
],
"devDependencies": {
"@types/chai": "^3.4.34",
"@types/jsdom": "^11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "^6.0.41",
"@types/node": "6.0.108",
"@types/text-encoding": "0.0.32",
"browserify": "^13.3.0",
"chai": "3.5.0",
"docdash": "0.4.0",
"express": "4.13.4",
"express-ws": "2.0.0-rc.1",
"fs-extra": "^1.0.0",
@@ -65,9 +32,11 @@
"merge-stream": "^1.0.1",
"node-pty": "^0.7.2",
"nodemon": "1.10.2",
"npm-run-all": "^4.1.2",
"sorcery": "^0.10.0",
"tslint": "^4.0.2",
"typescript": "~2.4.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "~2.7.1",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^3.10.0",
@@ -75,17 +44,17 @@
"zmodem.js": "^0.1.5"
},
"scripts": {
"prestart": "gulp webpack",
"start": "node demo/app",
"prestart-zmodem": "npm run build",
"start-zmodem": "node build/addons/zmodem/demo/app",
"lint": "tslint src/*.ts src/**/*.ts",
"test": "gulp test",
"start-zmodem": "node demo/zmodem/app",
"lint": "tslint 'src/**/*.ts'",
"test": "npm-run-all mocha lint",
"mocha": "gulp test",
"build:docs": "jsdoc -c jsdoc.json",
"tsc": "tsc",
"build": "gulp build",
"prepublish": "npm run build",
"coveralls": "gulp coveralls",
"webpack": "gulp webpack"
},
"dependencies": {}
"webpack": "gulp webpack",
"watch": "gulp watch"
}
}
+282
View File
@@ -0,0 +1,282 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as Strings from './Strings';
import { ITerminal, IBuffer } from './Types';
import { isMac } from './shared/utils/Browser';
import { RenderDebouncer } from './utils/RenderDebouncer';
import { addDisposableListener } from './utils/Dom';
import { IDisposable } from 'xterm';
const MAX_ROWS_TO_READ = 20;
const enum BoundaryPosition {
TOP,
BOTTOM
}
export class AccessibilityManager implements IDisposable {
private _accessibilityTreeRoot: HTMLElement;
private _rowContainer: HTMLElement;
private _rowElements: HTMLElement[];
private _liveRegion: HTMLElement;
private _liveRegionLineCount: number = 0;
private _renderRowsDebouncer: RenderDebouncer;
private _topBoundaryFocusListener: (e: FocusEvent) => void;
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
private _disposables: IDisposable[] = [];
/**
* This queue has a character pushed to it for keys that are pressed, if the
* next character added to the terminal is equal to the key char then it is
* not announced (added to live region) because it has already been announced
* by the textarea event (which cannot be canceled). There are some race
* condition cases if there is typing while data is streaming, but this covers
* the main case of typing into the prompt and inputting the answer to a
* question (Y/N, etc.).
*/
private _charsToConsume: string[] = [];
constructor(private _terminal: ITerminal) {
this._accessibilityTreeRoot = document.createElement('div');
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
this._rowContainer = document.createElement('div');
this._rowContainer.classList.add('xterm-accessibility-tree');
this._rowElements = [];
for (let i = 0; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
this._accessibilityTreeRoot.appendChild(this._rowContainer);
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._refreshRows();
this._liveRegion = document.createElement('div');
this._liveRegion.classList.add('live-region');
this._liveRegion.setAttribute('aria-live', 'assertive');
this._accessibilityTreeRoot.appendChild(this._liveRegion);
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
this._disposables.push(this._renderRowsDebouncer);
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
// Line feed is an issue as the prompt won't be read out after a command is run
this._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
// should be listened to instead? That would mean that the order of events are always
// guarenteed
this._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
// This shouldn't be needed on modern browsers but is present in case the
// media query that drives the dprchange event isn't supported
this._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
}
public dispose(): void {
this._disposables.forEach(d => d.dispose());
this._disposables.length = 0;
this._terminal.element.removeChild(this._accessibilityTreeRoot);
this._rowElements.length = 0;
}
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
const boundaryElement = <HTMLElement>e.target;
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
// Don't scroll if the buffer top has reached the end in that direction
const posInSet = boundaryElement.getAttribute('aria-posinset');
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
if (posInSet === lastRowPos) {
return;
}
// Don't scroll when the last focused item was not the second row (focus is going the other
// direction)
if (e.relatedTarget !== beforeBoundaryElement) {
return;
}
// Remove old boundary element from array
let topBoundaryElement: HTMLElement;
let bottomBoundaryElement: HTMLElement;
if (position === BoundaryPosition.TOP) {
topBoundaryElement = boundaryElement;
bottomBoundaryElement = this._rowElements.pop()!;
this._rowContainer.removeChild(bottomBoundaryElement);
} else {
topBoundaryElement = this._rowElements.shift()!;
bottomBoundaryElement = boundaryElement;
this._rowContainer.removeChild(topBoundaryElement);
}
// Remove listeners from old boundary elements
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
// Add new element to array/DOM
if (position === BoundaryPosition.TOP) {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.unshift(newElement);
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
} else {
const newElement = this._createAccessibilityTreeNode();
this._rowElements.push(newElement);
this._rowContainer.appendChild(newElement);
}
// Add listeners to new boundary elements
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
// Scroll up
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
// Focus new boundary before element
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
// Prevent the standard behavior
e.preventDefault();
e.stopImmediatePropagation();
}
private _onResize(cols: number, rows: number): void {
// Remove bottom boundary listener
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
// Grow rows as required
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
this._rowElements[i] = this._createAccessibilityTreeNode();
this._rowContainer.appendChild(this._rowElements[i]);
}
// Shrink rows as required
while (this._rowElements.length > rows) {
this._rowContainer.removeChild(this._rowElements.pop()!);
}
// Add bottom boundary listener
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
this._refreshRowsDimensions();
}
public _createAccessibilityTreeNode(): HTMLElement {
const element = document.createElement('div');
element.setAttribute('role', 'listitem');
element.tabIndex = -1;
this._refreshRowDimensions(element);
return element;
}
private _onTab(spaceCount: number): void {
for (let i = 0; i < spaceCount; i++) {
this._onChar(' ');
}
}
private _onChar(char: string): void {
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
if (this._charsToConsume.length > 0) {
// Have the screen reader ignore the char if it was just input
const shiftedChar = this._charsToConsume.shift();
if (shiftedChar !== char) {
this._announceCharacter(char);
}
} else {
this._announceCharacter(char);
}
if (char === '\n') {
this._liveRegionLineCount++;
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
this._liveRegion.textContent += Strings.tooMuchOutput;
}
}
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
setTimeout(() => {
this._accessibilityTreeRoot.appendChild(this._liveRegion);
}, 0);
}
}
}
}
private _clearLiveRegion(): void {
this._liveRegion.textContent = '';
this._liveRegionLineCount = 0;
// Only detach/attach on mac as otherwise messages can go unaccounced
if (isMac) {
if (this._liveRegion.parentNode) {
this._accessibilityTreeRoot.removeChild(this._liveRegion);
}
}
}
private _onKey(keyChar: string): void {
this._clearLiveRegion();
this._charsToConsume.push(keyChar);
}
private _refreshRows(start?: number, end?: number): void {
this._renderRowsDebouncer.refresh(start, end);
}
private _renderRows(start: number, end: number): void {
const buffer: IBuffer = this._terminal.buffer;
const setSize = buffer.lines.length.toString();
for (let i = start; i <= end; i++) {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
element.setAttribute('aria-posinset', posInSet);
element.setAttribute('aria-setsize', setSize);
}
}
private _refreshRowsDimensions(): void {
if (!this._terminal.renderer.dimensions.actualCellHeight) {
return;
}
for (let i = 0; i < this._terminal.rows; i++) {
this._refreshRowDimensions(this._rowElements[i]);
}
}
private _refreshRowDimensions(element: HTMLElement): void {
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
}
private _announceCharacter(char: string): void {
if (char === ' ') {
// Always use nbsp for spaces in order to preserve the space between characters in
// voiceover's caption window
this._liveRegion.innerHTML += '&nbsp;';
} else {
this._liveRegion.textContent += char;
}
}
}
+25 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { Buffer } from './Buffer';
import { CircularList } from './utils/CircularList';
import { MockTerminal } from './utils/TestUtils.test';
@@ -188,4 +188,28 @@ describe('Buffer', () => {
assert.equal(buffer.lines.maxLength, INIT_ROWS / 2);
});
});
describe('addMarker', () => {
it('should adjust a marker line when the buffer is trimmed', () => {
terminal.options.scrollback = 0;
buffer = new Buffer(terminal, true);
buffer.fillViewportRows();
const marker = buffer.addMarker(buffer.lines.length - 1);
assert.equal(marker.line, buffer.lines.length - 1);
buffer.lines.emit('trim', 1);
assert.equal(marker.line, buffer.lines.length - 2);
});
it('should dispose of a marker if it is trimmed off the buffer', () => {
terminal.options.scrollback = 0;
buffer = new Buffer(terminal, true);
buffer.fillViewportRows();
assert.equal(buffer.markers.length, 0);
const marker = buffer.addMarker(0);
assert.equal(marker.isDisposed, false);
assert.equal(buffer.markers.length, 1);
buffer.lines.emit('trim', 1);
assert.equal(marker.isDisposed, true);
assert.equal(buffer.markers.length, 0);
});
});
});
+76 -39
View File
@@ -3,9 +3,10 @@
* @license MIT
*/
import { ITerminal, IBuffer } from './Interfaces';
import { CircularList } from './utils/CircularList';
import { LineData, CharData } from './Types';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { EventEmitter } from './EventEmitter';
import { IDisposable, IMarker } from 'xterm';
export const CHAR_DATA_ATTR_INDEX = 0;
export const CHAR_DATA_CHAR_INDEX = 1;
@@ -21,8 +22,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
* - scroll position
*/
export class Buffer implements IBuffer {
private _lines: CircularList<LineData>;
public lines: CircularList<LineData>;
public ydisp: number;
public ybase: number;
public y: number;
@@ -32,6 +32,7 @@ export class Buffer implements IBuffer {
public tabs: any;
public savedY: number;
public savedX: number;
public markers: Marker[] = [];
/**
* Create a new Buffer.
@@ -46,10 +47,6 @@ export class Buffer implements IBuffer {
this.clear();
}
public get lines(): CircularList<LineData> {
return this._lines;
}
public get hasScrollback(): boolean {
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
}
@@ -79,7 +76,7 @@ export class Buffer implements IBuffer {
* Fills the buffer's viewport with blank lines.
*/
public fillViewportRows(): void {
if (this._lines.length === 0) {
if (this.lines.length === 0) {
let i = this._terminal.rows;
while (i--) {
this.lines.push(this._terminal.blankLine());
@@ -95,7 +92,7 @@ export class Buffer implements IBuffer {
this.ybase = 0;
this.y = 0;
this.x = 0;
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
this.scrollTop = 0;
this.scrollBottom = this._terminal.rows - 1;
this.setupTabStops();
@@ -110,24 +107,19 @@ export class Buffer implements IBuffer {
// Increase max length if needed before adjustments to allow space to fill
// as required.
const newMaxLength = this._getCorrectBufferLength(newRows);
if (newMaxLength > this._lines.maxLength) {
this._lines.maxLength = newMaxLength;
if (newMaxLength > this.lines.maxLength) {
this.lines.maxLength = newMaxLength;
}
// The following adjustments should only happen if the buffer has been
// initialized/filled.
if (this._lines.length > 0) {
if (this.lines.length > 0) {
// Deal with columns increasing (we don't do anything when columns reduce)
if (this._terminal.cols < newCols) {
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
for (let i = 0; i < this._lines.length; i++) {
// TODO: This should be removed, with tests setup for the case that was
// causing the underlying bug, see https://github.com/sourcelair/xterm.js/issues/824
if (this._lines.get(i) === undefined) {
this._lines.set(i, this._terminal.blankLine(undefined, undefined, newCols));
}
while (this._lines.get(i).length < newCols) {
this._lines.get(i).push(ch);
for (let i = 0; i < this.lines.length; i++) {
while (this.lines.get(i).length < newCols) {
this.lines.get(i).push(ch);
}
}
}
@@ -136,8 +128,8 @@ export class Buffer implements IBuffer {
let addToY = 0;
if (this._terminal.rows < newRows) {
for (let y = this._terminal.rows; y < newRows; y++) {
if (this._lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
if (this.lines.length < newRows + this.ybase) {
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
// There is room above the buffer and there are no empty elements below the line,
// scroll up
this.ybase--;
@@ -149,16 +141,16 @@ export class Buffer implements IBuffer {
} else {
// Add a blank line if there is no buffer left at the top to scroll to, or if there
// are blank lines after the cursor
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
}
}
}
} else { // (this._terminal.rows >= newRows)
for (let y = this._terminal.rows; y > newRows; y--) {
if (this._lines.length > newRows + this.ybase) {
if (this._lines.length > this.ybase + this.y + 1) {
if (this.lines.length > newRows + this.ybase) {
if (this.lines.length > this.ybase + this.y + 1) {
// The line is a blank line below the cursor, remove it
this._lines.pop();
this.lines.pop();
} else {
// The line is the cursor, scroll down
this.ybase++;
@@ -170,28 +162,25 @@ export class Buffer implements IBuffer {
// Reduce max length if needed after adjustments, this is done after as it
// would otherwise cut data from the bottom of the buffer.
if (newMaxLength < this._lines.maxLength) {
if (newMaxLength < this.lines.maxLength) {
// Trim from the top of the buffer and adjust ybase and ydisp.
const amountToTrim = this._lines.length - newMaxLength;
const amountToTrim = this.lines.length - newMaxLength;
if (amountToTrim > 0) {
this._lines.trimStart(amountToTrim);
this.lines.trimStart(amountToTrim);
this.ybase = Math.max(this.ybase - amountToTrim, 0);
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
}
this._lines.maxLength = newMaxLength;
this.lines.maxLength = newMaxLength;
}
// Make sure that the cursor stays on screen
if (this.y >= newRows) {
this.y = newRows - 1;
}
this.x = Math.min(this.x, newCols - 1);
this.y = Math.min(this.y, newRows - 1);
if (addToY) {
this.y += addToY;
}
if (this.x >= newCols) {
this.x = newCols - 1;
}
this.savedY = Math.min(this.savedY, newRows - 1);
this.savedX = Math.min(this.savedX, newCols - 1);
this.scrollTop = 0;
}
@@ -222,7 +211,10 @@ export class Buffer implements IBuffer {
// needed here because some chars are 0 characters long (eg. after wide
// chars) and some chars are longer than 1 characters long (eg. emojis).
let startIndex = startCol;
endCol = endCol || line.length;
// Only set endCol to the line length when it is null. 0 is a valid column.
if (endCol === null) {
endCol = line.length;
}
let endIndex = endCol;
for (let i = 0; i < line.length; i++) {
@@ -309,4 +301,49 @@ export class Buffer implements IBuffer {
while (!this.tabs[++x] && x < this._terminal.cols);
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
}
public addMarker(y: number): Marker {
const marker = new Marker(y);
this.markers.push(marker);
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
marker.line -= amount;
// The marker should be disposed when the line is trimmed from the buffer
if (marker.line < 0) {
marker.dispose();
}
}));
marker.on('dispose', () => this._removeMarker(marker));
return marker;
}
private _removeMarker(marker: Marker): void {
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
this.markers.splice(this.markers.indexOf(marker), 1);
}
}
export class Marker extends EventEmitter implements IMarker {
private static NEXT_ID = 1;
private _id: number = Marker.NEXT_ID++;
public isDisposed: boolean = false;
public disposables: IDisposable[] = [];
public get id(): number { return this._id; }
constructor(
public line: number
) {
super();
}
public dispose(): void {
if (this.isDisposed) {
return;
}
this.isDisposed = true;
this.disposables.forEach(d => d.dispose());
this.disposables.length = 0;
this.emit('dispose');
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { BufferSet } from './BufferSet';
import { Buffer } from './Buffer';
import { MockTerminal } from './utils/TestUtils.test';
+15 -4
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, IBufferSet } from './Interfaces';
import { ITerminal, IBufferSet } from './Types';
import { Buffer } from './Buffer';
import { EventEmitter } from './EventEmitter';
@@ -61,24 +61,35 @@ export class BufferSet extends EventEmitter implements IBufferSet {
* Sets the normal Buffer of the BufferSet as its currently active Buffer
*/
public activateNormalBuffer(): void {
if (this._activeBuffer === this._normal) {
return;
}
// The alt buffer should always be cleared when we switch to the normal
// buffer. This frees up memory since the alt buffer should always be new
// when activated.
this._alt.clear();
this._activeBuffer = this._normal;
this.emit('activate', this._normal);
this.emit('activate', {
activeBuffer: this._normal,
inactiveBuffer: this._alt
});
}
/**
* Sets the alt Buffer of the BufferSet as its currently active Buffer
*/
public activateAltBuffer(): void {
if (this._activeBuffer === this._alt) {
return;
}
// Since the alt buffer is always cleared when the normal buffer is
// activated, we want to fill it when switching to it.
this._alt.fillViewportRows();
this._activeBuffer = this._alt;
this.emit('activate', this._alt);
this.emit('activate', {
activeBuffer: this._alt,
inactiveBuffer: this._normal
});
}
/**
+23 -13
View File
@@ -49,7 +49,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
];
const COMBINING_HIGH = [
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
@@ -63,28 +63,33 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
let min = 0;
let max = data.length - 1;
let mid;
if (ucs < data[0][0] || ucs > data[max][1])
if (ucs < data[0][0] || ucs > data[max][1]) {
return false;
}
while (max >= min) {
mid = (min + max) >> 1;
if (ucs > data[mid][1])
if (ucs > data[mid][1]) {
min = mid + 1;
else if (ucs < data[mid][0])
} else if (ucs < data[mid][0]) {
max = mid - 1;
else
} else {
return true;
}
}
return false;
}
function wcwidthBMP(ucs: number): number {
// test for 8-bit control characters
if (ucs === 0)
if (ucs === 0) {
return opts.nul;
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0))
}
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
return opts.control;
}
// binary search in table of non-spacing characters
if (bisearch(ucs, COMBINING_BMP))
if (bisearch(ucs, COMBINING_BMP)) {
return 0;
}
// if we arrive here, ucs is not a combining or C0/C1 control character
if (isWideBMP(ucs)) {
return 2;
@@ -106,8 +111,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
(ucs >= 0xffe0 && ucs <= 0xffe6)));
}
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
if (bisearch(ucs, COMBINING_HIGH))
if (bisearch(ucs, COMBINING_HIGH)) {
return 0;
}
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
return 2;
}
@@ -128,8 +134,9 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
for (let i = 0; i < CONTAINERSIZE; ++i) {
let num = 0;
let pos = CODEPOINTS_PER_ITEM;
while (pos--)
while (pos--) {
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
}
table[i] = num;
}
return table;
@@ -148,13 +155,16 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
return function (num: number): number {
num = num | 0; // get asm.js like optimization under V8
if (num < 32)
if (num < 32) {
return control | 0;
if (num < 127)
}
if (num < 127) {
return 1;
}
let t = table || init_table();
if (num < 65536)
if (num < 65536) {
return t[num >> 4] >> ((num & 15) << 1) & 3;
}
// do a full search for high codepoints
return wcwidthHigh(num);
};
+3 -3
View File
@@ -3,19 +3,19 @@
* @license MIT
*/
import { Charset } from './Types';
import { ICharset } from './Types';
/**
* The character sets supported by the terminal. These enable several languages
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported.
*/
export const CHARSETS: { [key: string]: Charset } = {};
export const CHARSETS: { [key: string]: ICharset } = {};
/**
* The default character set, US.
*/
export const DEFAULT_CHARSET: Charset = CHARSETS['B'];
export const DEFAULT_CHARSET: ICharset = CHARSETS['B'];
/**
* DEC Special Character and Line Drawing Set.
+1 -1
View File
@@ -17,7 +17,7 @@ describe('CompositionHelper', () => {
compositionView = {
classList: {
add: () => {},
remove: () => {},
remove: () => {}
},
getBoundingClientRect: () => {
return { width: 0 };
+66 -67
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
interface IPosition {
start: number;
@@ -20,43 +20,43 @@ export class CompositionHelper {
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
* IME. This variable determines whether the compositionText should be displayed on the UI.
*/
private isComposing: boolean;
private _isComposing: boolean;
/**
* The position within the input textarea's value of the current composition.
*/
private compositionPosition: IPosition;
private _compositionPosition: IPosition;
/**
* Whether a composition is in the process of being sent, setting this to false will cancel any
* in-progress composition.
*/
private isSendingComposition: boolean;
private _isSendingComposition: boolean;
/**
* Creates a new CompositionHelper.
* @param textarea The textarea that xterm uses for input.
* @param compositionView The element to display the in-progress composition in.
* @param terminal The Terminal to forward the finished composition to.
* @param _textarea The textarea that xterm uses for input.
* @param _compositionView The element to display the in-progress composition in.
* @param _terminal The Terminal to forward the finished composition to.
*/
constructor(
private textarea: HTMLTextAreaElement,
private compositionView: HTMLElement,
private terminal: ITerminal
private _textarea: HTMLTextAreaElement,
private _compositionView: HTMLElement,
private _terminal: ITerminal
) {
this.isComposing = false;
this.isSendingComposition = false;
this.compositionPosition = { start: null, end: null };
this._isComposing = false;
this._isSendingComposition = false;
this._compositionPosition = { start: null, end: null };
}
/**
* Handles the compositionstart event, activating the composition view.
*/
public compositionstart(): void {
this.isComposing = true;
this.compositionPosition.start = this.textarea.value.length;
this.compositionView.textContent = '';
this.compositionView.classList.add('active');
this._isComposing = true;
this._compositionPosition.start = this._textarea.value.length;
this._compositionView.textContent = '';
this._compositionView.classList.add('active');
}
/**
@@ -64,10 +64,10 @@ export class CompositionHelper {
* @param {CompositionEvent} ev The event.
*/
public compositionupdate(ev: CompositionEvent): void {
this.compositionView.textContent = ev.data;
this._compositionView.textContent = ev.data;
this.updateCompositionElements();
setTimeout(() => {
this.compositionPosition.end = this.textarea.value.length;
this._compositionPosition.end = this._textarea.value.length;
}, 0);
}
@@ -76,7 +76,7 @@ export class CompositionHelper {
* the handler.
*/
public compositionend(): void {
this.finalizeComposition(true);
this._finalizeComposition(true);
}
/**
@@ -85,24 +85,23 @@ export class CompositionHelper {
* @return Whether the Terminal should continue processing the keydown event.
*/
public keydown(ev: KeyboardEvent): boolean {
if (this.isComposing || this.isSendingComposition) {
if (this._isComposing || this._isSendingComposition) {
if (ev.keyCode === 229) {
// Continue composing if the keyCode is the "composition character"
return false;
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
// Continue composing if the keyCode is a modifier key
return false;
} else {
// Finish composition immediately. This is mainly here for the case where enter is
// pressed and the handler needs to be triggered before the command is executed.
this.finalizeComposition(false);
}
// Finish composition immediately. This is mainly here for the case where enter is
// pressed and the handler needs to be triggered before the command is executed.
this._finalizeComposition(false);
}
if (ev.keyCode === 229) {
// If the "composition character" is used but gets to this point it means a non-composition
// character (eg. numbers and punctuation) was pressed when the IME was active.
this.handleAnyTextareaChanges();
this._handleAnyTextareaChanges();
return false;
}
@@ -117,22 +116,22 @@ export class CompositionHelper {
* compositionend event is triggered, such as enter, so that the composition is send before
* the command is executed.
*/
private finalizeComposition(waitForPropogation: boolean): void {
this.compositionView.classList.remove('active');
this.isComposing = false;
this.clearTextareaPosition();
private _finalizeComposition(waitForPropogation: boolean): void {
this._compositionView.classList.remove('active');
this._isComposing = false;
this._clearTextareaPosition();
if (!waitForPropogation) {
// Cancel any delayed composition send requests and send the input immediately.
this.isSendingComposition = false;
const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
this.terminal.handler(input);
this._isSendingComposition = false;
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
this._terminal.handler(input);
} else {
// Make a deep copy of the composition position here as a new compositionstart event may
// fire before the setTimeout executes.
const currentCompositionPosition = {
start: this.compositionPosition.start,
end: this.compositionPosition.end,
start: this._compositionPosition.start,
end: this._compositionPosition.end
};
// Since composition* events happen before the changes take place in the textarea on most
@@ -143,22 +142,22 @@ export class CompositionHelper {
// - The last compositionupdate event's data property does not always accurately describe
// the character, a counter example being Korean where an ending consonsant can move to
// the following character if the following input is a vowel.
this.isSendingComposition = true;
this._isSendingComposition = true;
setTimeout(() => {
// Ensure that the input has not already been sent
if (this.isSendingComposition) {
this.isSendingComposition = false;
if (this._isSendingComposition) {
this._isSendingComposition = false;
let input;
if (this.isComposing) {
if (this._isComposing) {
// Use the end position to get the string if a new composition has started.
input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
} else {
// Don't use the end position here in order to pick up any characters after the
// composition has finished, for example when typing a non-composition character
// (eg. 2) after a composition character.
input = this.textarea.value.substring(currentCompositionPosition.start);
input = this._textarea.value.substring(currentCompositionPosition.start);
}
this.terminal.handler(input);
this._terminal.handler(input);
}
}, 0);
}
@@ -170,15 +169,15 @@ export class CompositionHelper {
* character" (229) is triggered, in order to allow non-composition text to be entered when an
* IME is active.
*/
private handleAnyTextareaChanges(): void {
const oldValue = this.textarea.value;
private _handleAnyTextareaChanges(): void {
const oldValue = this._textarea.value;
setTimeout(() => {
// Ignore if a composition has started since the timeout
if (!this.isComposing) {
const newValue = this.textarea.value;
if (!this._isComposing) {
const newValue = this._textarea.value;
const diff = newValue.replace(oldValue, '');
if (diff.length > 0) {
this.terminal.handler(diff);
this._terminal.handler(diff);
}
}
}, 0);
@@ -191,40 +190,40 @@ export class CompositionHelper {
* necessary as the IME events across browsers are not consistently triggered.
*/
public updateCompositionElements(dontRecurse?: boolean): void {
if (!this.isComposing) {
if (!this._isComposing) {
return;
}
if (this.terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
const cursorTop = this.terminal.buffer.y * cellHeight;
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
if (this._terminal.buffer.isCursorInViewport) {
const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight);
const cursorTop = this._terminal.buffer.y * cellHeight;
const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width;
this.compositionView.style.left = cursorLeft + 'px';
this.compositionView.style.top = cursorTop + 'px';
this.compositionView.style.height = cellHeight + 'px';
this.compositionView.style.lineHeight = cellHeight + 'px';
this._compositionView.style.left = cursorLeft + 'px';
this._compositionView.style.top = cursorTop + 'px';
this._compositionView.style.height = cellHeight + 'px';
this._compositionView.style.lineHeight = cellHeight + 'px';
// Sync the textarea to the exact position of the composition view so the IME knows where the
// text is.
const compositionViewBounds = this.compositionView.getBoundingClientRect();
this.textarea.style.left = cursorLeft + 'px';
this.textarea.style.top = cursorTop + 'px';
this.textarea.style.width = compositionViewBounds.width + 'px';
this.textarea.style.height = compositionViewBounds.height + 'px';
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
const compositionViewBounds = this._compositionView.getBoundingClientRect();
this._textarea.style.left = cursorLeft + 'px';
this._textarea.style.top = cursorTop + 'px';
this._textarea.style.width = compositionViewBounds.width + 'px';
this._textarea.style.height = compositionViewBounds.height + 'px';
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
}
if (!dontRecurse) {
setTimeout(() => this.updateCompositionElements(true), 0);
}
};
}
/**
* Clears the textarea's position so that the cursor does not blink on IE.
* @private
*/
private clearTextareaPosition(): void {
this.textarea.style.left = '';
this.textarea.style.top = '';
};
private _clearTextareaPosition(): void {
this._textarea.style.left = '';
this._textarea.style.top = '';
}
}
+1 -1
View File
@@ -76,4 +76,4 @@ export namespace C0 {
export const SP = '\x20';
/** Delete (Caret = ^?) */
export const DEL = '\x7f';
};
}
-12
View File
@@ -13,18 +13,6 @@ describe('EventEmitter', () => {
eventEmitter = new EventEmitter();
});
describe('once', () => {
it('should trigger the listener only once', () => {
let count = 0;
const listener = () => count++;
eventEmitter.once('test', listener);
eventEmitter.emit('test');
assert.equal(count, 1);
eventEmitter.emit('test');
assert.equal(count, 1);
});
});
describe('emit', () => {
it('should emit events to listeners', () => {
let count1 = 0;
+28 -18
View File
@@ -3,10 +3,11 @@
* @license MIT
*/
import { IEventEmitter, IListenerType } from './Interfaces';
import { XtermListener } from './Types';
import { IEventEmitter, IDisposable } from 'xterm';
export class EventEmitter implements IEventEmitter {
private _events: {[type: string]: IListenerType[]};
export class EventEmitter implements IEventEmitter, IDisposable {
private _events: {[type: string]: XtermListener[]};
constructor() {
// Restore the previous events if available, this will happen if the
@@ -14,12 +15,31 @@ export class EventEmitter implements IEventEmitter {
this._events = this._events || {};
}
public on(type: string, listener: IListenerType): void {
public on(type: string, listener: XtermListener): void {
this._events[type] = this._events[type] || [];
this._events[type].push(listener);
}
public off(type: string, listener: IListenerType): void {
/**
* Adds a disposabe listener to the EventEmitter, returning the disposable.
* @param type The event type.
* @param handler The handler for the listener.
*/
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
this.on(type, handler);
return {
dispose: () => {
if (!handler) {
// Already disposed
return;
}
this.off(type, handler);
handler = null;
}
};
}
public off(type: string, listener: XtermListener): void {
if (!this._events[type]) {
return;
}
@@ -28,7 +48,7 @@ export class EventEmitter implements IEventEmitter {
let i = obj.length;
while (i--) {
if (obj[i] === listener || obj[i].listener === listener) {
if (obj[i] === listener) {
obj.splice(i, 1);
return;
}
@@ -41,16 +61,6 @@ export class EventEmitter implements IEventEmitter {
}
}
public once(type: string, listener: IListenerType): void {
function on(): void {
let args = Array.prototype.slice.call(arguments);
this.off(type, on);
listener.apply(this, args);
}
(<any>on).listener = listener;
this.on(type, on);
}
public emit(type: string, ...args: any[]): void {
if (!this._events[type]) {
return;
@@ -61,11 +71,11 @@ export class EventEmitter implements IEventEmitter {
}
}
public listeners(type: string): IListenerType[] {
public listeners(type: string): XtermListener[] {
return this._events[type] || [];
}
protected destroy(): void {
public dispose(): void {
this._events = {};
}
}
+127 -78
View File
@@ -4,10 +4,9 @@
* @license MIT
*/
import { IInputHandler, ITerminal, IInputHandlingTerminal } from './Interfaces';
import { CharData, IInputHandler, IInputHandlingTerminal } from './Types';
import { C0 } from './EscapeSequences';
import { DEFAULT_CHARSET } from './Charsets';
import { CharData } from './Types';
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
import { FLAGS } from './renderer/Types';
import { wcwidth } from './CharWidth';
@@ -24,84 +23,93 @@ export class InputHandler implements IInputHandler {
public addChar(char: string, code: number): void {
if (char >= ' ') {
// make buffer local for faster access
const buffer = this._terminal.buffer;
// calculate print space
// expensive call, therefore we save width in line buffer
const ch_width = wcwidth(code);
const chWidth = wcwidth(code);
if (this._terminal.charset && this._terminal.charset[char]) {
char = this._terminal.charset[char];
}
let row = this._terminal.buffer.y + this._terminal.buffer.ybase;
if (this._terminal.options.screenReaderMode) {
this._terminal.emit('a11y.char', char);
}
let row = buffer.y + buffer.ybase;
// insert combining char in last cell
// FIXME: needs handling after cursor jumps
if (!ch_width && this._terminal.buffer.x) {
if (!chWidth && buffer.x) {
// dont overflow left
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1]) {
if (!this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
if (buffer.lines.get(row)[buffer.x - 1]) {
if (!buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_WIDTH_INDEX]) {
// found empty cell after fullwidth, need to go 2 cells back
if (this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2]) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 2][3] = char.charCodeAt(0);
if (buffer.lines.get(row)[buffer.x - 2]) {
buffer.lines.get(row)[buffer.x - 2][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[buffer.x - 2][3] = char.charCodeAt(0);
}
} else {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x - 1][3] = char.charCodeAt(0);
buffer.lines.get(row)[buffer.x - 1][CHAR_DATA_CHAR_INDEX] += char;
buffer.lines.get(row)[buffer.x - 1][3] = char.charCodeAt(0);
}
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(buffer.y);
}
return;
}
// goto next line if ch would overflow
// TODO: needs a global min terminal width of 2
if (this._terminal.buffer.x + ch_width - 1 >= this._terminal.cols) {
if (buffer.x + chWidth - 1 >= this._terminal.cols) {
// autowrap - DECAWM
if (this._terminal.wraparoundMode) {
this._terminal.buffer.x = 0;
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
buffer.x = 0;
buffer.y++;
if (buffer.y > buffer.scrollBottom) {
buffer.y--;
this._terminal.scroll(true);
} else {
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
(<any>this._terminal.buffer.lines.get(this._terminal.buffer.y)).isWrapped = true;
(<any>buffer.lines.get(buffer.y)).isWrapped = true;
}
} else {
if (ch_width === 2) // FIXME: check for xterm behavior
if (chWidth === 2) { // FIXME: check for xterm behavior
return;
}
}
}
row = this._terminal.buffer.y + this._terminal.buffer.ybase;
row = buffer.y + buffer.ybase;
// insert mode: move characters to right
if (this._terminal.insertMode) {
// do this twice for a fullwidth char
for (let moves = 0; moves < ch_width; ++moves) {
for (let moves = 0; moves < chWidth; ++moves) {
// remove last cell, if it's width is 0
// we have to adjust the second last cell as well
const removed = this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).pop();
const removed = buffer.lines.get(buffer.y + buffer.ybase).pop();
if (removed[CHAR_DATA_WIDTH_INDEX] === 0
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2]
&& this._terminal.buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
&& buffer.lines.get(row)[this._terminal.cols - 2]
&& buffer.lines.get(row)[this._terminal.cols - 2][CHAR_DATA_WIDTH_INDEX] === 2) {
buffer.lines.get(row)[this._terminal.cols - 2] = [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)];
}
// insert empty cell at cursor
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
buffer.lines.get(row).splice(buffer.x, 0, [this._terminal.curAttr, ' ', 1, ' '.charCodeAt(0)]);
}
}
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, char, ch_width, char.charCodeAt(0)];
this._terminal.buffer.x++;
this._terminal.updateRange(this._terminal.buffer.y);
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, char, chWidth, char.charCodeAt(0)];
buffer.x++;
this._terminal.updateRange(buffer.y);
// fullwidth char - set next cell width to zero and advance cursor
if (ch_width === 2) {
this._terminal.buffer.lines.get(row)[this._terminal.buffer.x] = [this._terminal.curAttr, '', 0, undefined];
this._terminal.buffer.x++;
if (chWidth === 2) {
buffer.lines.get(row)[buffer.x] = [this._terminal.curAttr, '', 0, undefined];
buffer.x++;
}
}
}
@@ -119,17 +127,20 @@ export class InputHandler implements IInputHandler {
* Line Feed or New Line (NL). (LF is Ctrl-J).
*/
public lineFeed(): void {
// make buffer local for faster access
const buffer = this._terminal.buffer;
if (this._terminal.convertEol) {
this._terminal.buffer.x = 0;
buffer.x = 0;
}
this._terminal.buffer.y++;
if (this._terminal.buffer.y > this._terminal.buffer.scrollBottom) {
this._terminal.buffer.y--;
buffer.y++;
if (buffer.y > buffer.scrollBottom) {
buffer.y--;
this._terminal.scroll();
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._terminal.buffer.x >= this._terminal.cols) {
this._terminal.buffer.x--;
if (buffer.x >= this._terminal.cols) {
buffer.x--;
}
/**
* This event is emitted whenever the terminal outputs a LF or NL.
@@ -162,7 +173,11 @@ export class InputHandler implements IInputHandler {
* Horizontal Tab (HT) (Ctrl-I).
*/
public tab(): void {
const originalX = this._terminal.buffer.x;
this._terminal.buffer.x = this._terminal.buffer.nextStop();
if (this._terminal.options.screenReaderMode) {
this._terminal.emit('a11y.tab', this._terminal.buffer.x - originalX);
}
}
/**
@@ -191,13 +206,16 @@ export class InputHandler implements IInputHandler {
let param = params[0];
if (param < 1) param = 1;
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row).splice(j++, 0, ch);
this._terminal.buffer.lines.get(row).pop();
buffer.lines.get(row).splice(j++, 0, ch);
buffer.lines.get(row).pop();
}
}
@@ -439,20 +457,24 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
let row: number = this._terminal.buffer.y + this._terminal.buffer.ybase;
let scrollBottomRowsOffset = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
let scrollBottomAbsolute = this._terminal.rows - 1 + this._terminal.buffer.ybase - scrollBottomRowsOffset + 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
let row: number = buffer.y + buffer.ybase;
let scrollBottomRowsOffset = this._terminal.rows - 1 - buffer.scrollBottom;
let scrollBottomAbsolute = this._terminal.rows - 1 + buffer.ybase - scrollBottomRowsOffset + 1;
while (param--) {
// test: echo -e '\e[44m\e[1L\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(scrollBottomAbsolute - 1, 1);
this._terminal.buffer.lines.splice(row, 0, this._terminal.blankLine(true));
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
buffer.lines.splice(row, 0, this._terminal.blankLine(true));
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -464,21 +486,25 @@ export class InputHandler implements IInputHandler {
if (param < 1) {
param = 1;
}
const row: number = this._terminal.buffer.y + this._terminal.buffer.ybase;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row: number = buffer.y + buffer.ybase;
let j: number;
j = this._terminal.rows - 1 - this._terminal.buffer.scrollBottom;
j = this._terminal.rows - 1 + this._terminal.buffer.ybase - j;
j = this._terminal.rows - 1 - buffer.scrollBottom;
j = this._terminal.rows - 1 + buffer.ybase - j;
while (param--) {
// test: echo -e '\e[44m\e[1M\e[0m'
// blankLine(true) - xterm/linux behavior
this._terminal.buffer.lines.splice(row, 1);
this._terminal.buffer.lines.splice(j, 0, this._terminal.blankLine(true));
buffer.lines.splice(row, 1);
buffer.lines.splice(j, 0, this._terminal.blankLine(true));
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.y);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -491,14 +517,17 @@ export class InputHandler implements IInputHandler {
param = 1;
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param--) {
this._terminal.buffer.lines.get(row).splice(this._terminal.buffer.x, 1);
this._terminal.buffer.lines.get(row).push(ch);
buffer.lines.get(row).splice(buffer.x, 1);
buffer.lines.get(row).push(ch);
}
this._terminal.updateRange(this._terminal.buffer.y);
this._terminal.updateRange(buffer.y);
}
/**
@@ -506,13 +535,17 @@ export class InputHandler implements IInputHandler {
*/
public scrollUp(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 0, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -520,13 +553,17 @@ export class InputHandler implements IInputHandler {
*/
public scrollDown(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollBottom, 1);
this._terminal.buffer.lines.splice(this._terminal.buffer.ybase + this._terminal.buffer.scrollTop, 0, this._terminal.blankLine());
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, this._terminal.blankLine());
}
// this.maxRange();
this._terminal.updateRange(this._terminal.buffer.scrollTop);
this._terminal.updateRange(this._terminal.buffer.scrollBottom);
this._terminal.updateRange(buffer.scrollTop);
this._terminal.updateRange(buffer.scrollBottom);
}
/**
@@ -539,12 +576,15 @@ export class InputHandler implements IInputHandler {
param = 1;
}
const row = this._terminal.buffer.y + this._terminal.buffer.ybase;
let j = this._terminal.buffer.x;
// make buffer local for faster access
const buffer = this._terminal.buffer;
const row = buffer.y + buffer.ybase;
let j = buffer.x;
const ch: CharData = [this._terminal.eraseAttr(), ' ', 1, 32]; // xterm
while (param-- && j < this._terminal.cols) {
this._terminal.buffer.lines.get(row)[j++] = ch;
buffer.lines.get(row)[j++] = ch;
}
}
@@ -553,8 +593,12 @@ export class InputHandler implements IInputHandler {
*/
public cursorBackwardTab(params: number[]): void {
let param = params[0] || 1;
// make buffer local for faster access
const buffer = this._terminal.buffer;
while (param--) {
this._terminal.buffer.x = this._terminal.buffer.prevStop();
buffer.x = buffer.prevStop();
}
}
@@ -594,11 +638,15 @@ export class InputHandler implements IInputHandler {
*/
public repeatPrecedingCharacter(params: number[]): void {
let param = params[0] || 1;
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + this._terminal.buffer.y);
const ch = line[this._terminal.buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
// make buffer local for faster access
const buffer = this._terminal.buffer;
const line = buffer.lines.get(buffer.ybase + buffer.y);
const ch = line[buffer.x - 1] || [this._terminal.defAttr, ' ', 1, 32];
while (param--) {
line[this._terminal.buffer.x++] = ch;
line[buffer.x++] = ch;
}
}
@@ -928,7 +976,6 @@ export class InputHandler implements IInputHandler {
case 47: // alt screen buffer
case 1047: // alt screen buffer
this._terminal.buffers.activateAltBuffer();
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
break;
@@ -1100,7 +1147,6 @@ export class InputHandler implements IInputHandler {
// if (params[0] === 1049) {
// this.restoreCursor(params);
// }
this._terminal.selectionManager.setBuffer(this._terminal.buffer);
this._terminal.refresh(0, this._terminal.rows - 1);
this._terminal.viewport.syncScrollArea();
this._terminal.showCursor();
@@ -1217,6 +1263,9 @@ export class InputHandler implements IInputHandler {
} else if (p === 1) {
// bold text
flags |= FLAGS.BOLD;
} else if (p === 3) {
// italic text
flags |= FLAGS.ITALIC;
} else if (p === 4) {
// underlined text
flags |= FLAGS.UNDERLINE;
-354
View File
@@ -1,354 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ILinkMatcherOptions } from './Interfaces';
import { LinkMatcherHandler, LinkMatcherValidationCallback, Charset, LineData } from './Types';
import { IColorSet, IRenderer } from './renderer/Interfaces';
import { IMouseZoneManager } from './input/Interfaces';
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface ITerminal extends ILinkifierAccessor, IBufferAccessor, IElementAccessor, IEventEmitter {
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
textarea: HTMLTextAreaElement;
renderer: IRenderer;
rows: number;
cols: number;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
bracketedPasteMode: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
reset(): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
refresh(start: number, end: number): void;
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: Charset;
gcharset: number;
glevel: number;
charsets: Charset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: Charset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
}
export interface ITerminalOptions {
bellSound?: string;
bellStyle?: string;
cancelEvents?: boolean;
cols?: number;
convertEol?: boolean;
cursorBlink?: boolean;
cursorStyle?: string;
debug?: boolean;
disableStdin?: boolean;
enableBold?: boolean;
fontSize?: number;
fontFamily?: string;
handler?: (data: string) => void;
letterSpacing?: number;
lineHeight?: number;
rows?: number;
screenKeys?: boolean;
scrollback?: number;
tabStopWidth?: number;
termName?: string;
theme?: ITheme;
useFlowControl?: boolean;
}
export interface IBuffer {
lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface IViewport {
syncScrollArea(): void;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setBuffer(buffer: IBuffer): void;
setSelection(row: number, col: number, length: number): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
forEach: (callbackfn: (value: T, index: number) => void) => void;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface IEventEmitter {
on(type: string, listener: IListenerType): void;
off(type: string, listener: IListenerType): void;
emit(type: string, data?: any): void;
}
export interface IListenerType {
(data?: any): void;
listener?: (data?: any) => void;
};
export interface ILinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
}
/**
* Handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ITheme {
foreground?: string;
background?: string;
cursor?: string;
cursorAccent?: string;
selection?: string;
black?: string;
red?: string;
green?: string;
yellow?: string;
blue?: string;
magenta?: string;
cyan?: string;
white?: string;
brightBlack?: string;
brightRed?: string;
brightGreen?: string;
brightYellow?: string;
brightBlue?: string;
brightMagenta?: string;
brightCyan?: string;
brightWhite?: string;
}
+52 -34
View File
@@ -4,20 +4,19 @@
*/
import { assert } from 'chai';
import { ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Interfaces';
import { IMouseZoneManager, IMouseZone } from './input/Types';
import { ILinkMatcher, LineData, ITerminal } from './Types';
import { Linkifier } from './Linkifier';
import { LinkMatcher, LineData } from './Types';
import { IMouseZoneManager, IMouseZone } from './input/Interfaces';
import { MockBuffer } from './utils/TestUtils.test';
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
import { CircularList } from './utils/CircularList';
class TestLinkifier extends Linkifier {
constructor(_terminal: IBufferAccessor & IElementAccessor) {
constructor(_terminal: ITerminal) {
super(_terminal);
Linkifier.TIME_BEFORE_LINKIFY = 0;
}
public get linkMatchers(): LinkMatcher[] { return this._linkMatchers; }
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
public linkifyRows(): void { super.linkifyRows(0, this._terminal.buffer.lines.length - 1); }
}
@@ -33,16 +32,15 @@ class TestMouseZoneManager implements IMouseZoneManager {
}
describe('Linkifier', () => {
let terminal: IBufferAccessor & IElementAccessor;
let terminal: ITerminal;
let linkifier: TestLinkifier;
let mouseZoneManager: TestMouseZoneManager;
beforeEach(() => {
terminal = {
buffer: new MockBuffer(),
element: <HTMLElement>{}
};
terminal.buffer.lines = new CircularList<LineData>(20);
terminal = new MockTerminal();
terminal.cols = 100;
terminal.buffer = new MockBuffer();
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
terminal.buffer.ydisp = 0;
linkifier = new TestLinkifier(terminal);
mouseZoneManager = new TestMouseZoneManager();
@@ -60,17 +58,6 @@ describe('Linkifier', () => {
terminal.buffer.lines.push(stringToRow(text));
}
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
addRow(uri);
linkifier.linkifyRows();
setTimeout(() => {
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
done();
}, 0);
}
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
@@ -81,7 +68,25 @@ describe('Linkifier', () => {
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length);
assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length);
});
done();
}, 0);
}
function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
addRow(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
@@ -102,12 +107,6 @@ describe('Linkifier', () => {
linkifier.attachToDom(mouseZoneManager);
});
describe('http links', () => {
it('should allow ~ character in URI path', (done) => {
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
});
});
describe('link matcher', () => {
it('should match a single link', done => {
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
@@ -136,6 +135,24 @@ describe('Linkifier', () => {
// character (U+1F537) which caused the path to be duplicated. See #642.
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
});
describe('multi-line links', () => {
it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done);
});
it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done);
});
it('should match links that start and end on line 2/2 of a wrapped line', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done);
});
it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => {
terminal.cols = 4;
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
});
});
});
describe('validationCallback', () => {
@@ -148,7 +165,8 @@ describe('Linkifier', () => {
assert.equal(mouseZoneManager.zones.length, 1);
assert.equal(mouseZoneManager.zones[0].x1, 1);
assert.equal(mouseZoneManager.zones[0].x2, 5);
assert.equal(mouseZoneManager.zones[0].y, 1);
assert.equal(mouseZoneManager.zones[0].y1, 1);
assert.equal(mouseZoneManager.zones[0].y2, 1);
// Fires done()
mouseZoneManager.zones[0].clickCallback(<any>{});
}
@@ -201,19 +219,19 @@ describe('Linkifier', () => {
it('should order the list from highest priority to lowest #1', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
it('should order the list from highest priority to lowest #2', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]);
});
it('should order items of equal priority in the order they are added', () => {
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
});
});
});
+66 -72
View File
@@ -3,36 +3,11 @@
* @license MIT
*/
import { ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Interfaces';
import { LinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEvent, LinkHoverEventTypes } from './Types';
import { IMouseZoneManager } from './input/Interfaces';
import { IMouseZoneManager } from './input/Types';
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
import { MouseZone } from './input/MouseZoneManager';
import { EventEmitter } from './EventEmitter';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
/**
* The ID of the built in http(s) link matcher.
*/
const HYPERTEXT_LINK_MATCHER_ID = 0;
/**
* The Linkifier applies links to rows shortly after they have been refreshed.
*/
@@ -44,22 +19,21 @@ export class Linkifier extends EventEmitter implements ILinkifier {
*/
protected static TIME_BEFORE_LINKIFY = 200;
protected _linkMatchers: LinkMatcher[] = [];
protected _linkMatchers: ILinkMatcher[] = [];
private _mouseZoneManager: IMouseZoneManager;
private _rowsTimeoutId: number;
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
private _nextLinkMatcherId = 0;
private _rowsToLinkify: {start: number, end: number};
constructor(
protected _terminal: IBufferAccessor & IElementAccessor
protected _terminal: ITerminal
) {
super();
this._rowsToLinkify = {
start: null,
end: null
};
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
}
/**
@@ -82,12 +56,12 @@ export class Linkifier extends EventEmitter implements ILinkifier {
}
// Increase range to linkify
if (!this._rowsToLinkify.start) {
if (this._rowsToLinkify.start === null) {
this._rowsToLinkify.start = start;
this._rowsToLinkify.end = end;
} else {
this._rowsToLinkify.start = this._rowsToLinkify.start < start ? this._rowsToLinkify.start : start;
this._rowsToLinkify.end = this._rowsToLinkify.end > end ? this._rowsToLinkify.end : end;
this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start);
this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end);
}
// Clear out any existing links on this row range
@@ -112,23 +86,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
this._rowsToLinkify.end = null;
}
/**
* Attaches a handler for hypertext links, overriding default <a> behavior for
* tandard http(s) links.
* @param handler The handler to use, this can be cleared with null.
*/
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
}
/**
* Attaches a validation callback for hypertext links.
* @param callback The callback to use, this can be cleared with null.
*/
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
}
/**
* Registers a link matcher, allowing custom link patterns to be matched and
* handled.
@@ -140,10 +97,10 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return The ID of the new matcher, this can be used to deregister.
*/
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
if (!handler) {
throw new Error('handler must be defined');
}
const matcher: LinkMatcher = {
const matcher: ILinkMatcher = {
id: this._nextLinkMatcherId++,
regex,
handler,
@@ -151,6 +108,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
validationCallback: options.validationCallback,
hoverTooltipCallback: options.tooltipCallback,
hoverLeaveCallback: options.leaveCallback,
willLinkActivate: options.willLinkActivate,
priority: options.priority || 0
};
this._addLinkMatcherToList(matcher);
@@ -163,7 +121,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* considered after older link matchers.
* @param matcher The link matcher to be added.
*/
private _addLinkMatcherToList(matcher: LinkMatcher): void {
private _addLinkMatcherToList(matcher: ILinkMatcher): void {
if (this._linkMatchers.length === 0) {
this._linkMatchers.push(matcher);
return;
@@ -185,8 +143,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @return Whether a link matcher was found and deregistered.
*/
public deregisterLinkMatcher(matcherId: number): boolean {
// ID 0 is the hypertext link matcher which cannot be deregistered
for (let i = 1; i < this._linkMatchers.length; i++) {
for (let i = 0; i < this._linkMatchers.length; i++) {
if (this._linkMatchers[i].id === matcherId) {
this._linkMatchers.splice(i, 1);
return true;
@@ -200,11 +157,32 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param rowIndex The index of the row to linkify.
*/
private _linkifyRow(rowIndex: number): void {
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
// Ensure the row exists
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
return;
}
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
// Only attempt to linkify rows that start in the viewport
if (rowIndex !== 0) {
return;
}
// If the first row is wrapped, backtrack to find the origin row and linkify that
do {
rowIndex--;
absoluteRowIndex--;
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
}
// Construct full unwrapped line text
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
let currentIndex = absoluteRowIndex + 1;
while (currentIndex < this._terminal.buffer.lines.length &&
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
}
for (let i = 0; i < this._linkMatchers.length; i++) {
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
}
@@ -219,11 +197,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param offset The how much of the row has already been linkified.
* @return The link element(s) that were added.
*/
private _doLinkifyRow(rowIndex: number, text: string, matcher: LinkMatcher, offset: number = 0): void {
// Iterate over nodes as we want to consider text nodes
let result = [];
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
// Find the first match
let match = text.match(matcher.regex);
if (!match || match.length === 0) {
@@ -264,11 +238,21 @@ export class Linkifier extends EventEmitter implements ILinkifier {
* @param uri The URI of the link.
* @param matcher The link matcher for the link.
*/
private _addLink(x: number, y: number, uri: string, matcher: LinkMatcher): void {
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
const x1 = x % this._terminal.cols;
const y1 = y + Math.floor(x / this._terminal.cols);
let x2 = (x1 + uri.length) % this._terminal.cols;
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
if (x2 === 0) {
x2 = this._terminal.cols;
y2--;
}
this._mouseZoneManager.add(new MouseZone(
x + 1,
x + 1 + uri.length,
y + 1,
x1 + 1,
y1 + 1,
x2 + 1,
y2 + 1,
e => {
if (matcher.handler) {
return matcher.handler(e, uri);
@@ -276,22 +260,32 @@ export class Linkifier extends EventEmitter implements ILinkifier {
window.open(uri, '_blank');
},
e => {
this.emit(LinkHoverEventTypes.HOVER, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = 'pointer';
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.add('xterm-cursor-pointer');
},
e => {
this.emit(LinkHoverEventTypes.TOOLTIP, <LinkHoverEvent>{ x, y, length: uri.length});
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
if (matcher.hoverTooltipCallback) {
matcher.hoverTooltipCallback(e, uri);
}
},
() => {
this.emit(LinkHoverEventTypes.LEAVE, <LinkHoverEvent>{ x, y, length: uri.length});
this._terminal.element.style.cursor = '';
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
this._terminal.element.classList.remove('xterm-cursor-pointer');
if (matcher.hoverLeaveCallback) {
matcher.hoverLeaveCallback();
}
},
e => {
if (matcher.willLinkActivate) {
return matcher.willLinkActivate(e, uri);
}
return true;
}
));
}
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
return { x1, y1, x2, y2, cols: this._terminal.cols };
}
}
+12 -5
View File
@@ -5,7 +5,7 @@
*/
import { C0 } from './EscapeSequences';
import { IInputHandler } from './Interfaces';
import { IInputHandler, IInputHandlingTerminal } from './Types';
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
@@ -150,7 +150,7 @@ csiStateHandler['s'] = (handler, params) => handler.saveCursor(params);
csiStateHandler['u'] = (handler, params) => handler.restoreCursor(params);
csiStateHandler[C0.CAN] = (handler, params, prefix, postfix, parser) => parser.setState(ParserState.NORMAL);
export enum ParserState {
export const enum ParserState {
NORMAL = 0,
ESCAPED = 1,
CSI_PARAM = 2,
@@ -185,7 +185,6 @@ export class Parser {
*/
public parse(data: string): ParserState {
const l = data.length;
let j;
let cs;
let ch;
let code;
@@ -224,8 +223,9 @@ export class Parser {
ch += data.charAt(this._position + 1);
}
// surrogate low - already handled above
if (0xDC00 <= code && code <= 0xDFFF)
if (0xDC00 <= code && code <= 0xDFFF) {
continue;
}
switch (this._state) {
case ParserState.NORMAL:
@@ -286,36 +286,43 @@ export class Parser {
// Single Shift Select of G2 Character Set
// ( SS2 is 0x8e). This affects next character only.
case 'N':
this._state = ParserState.NORMAL;
break;
// ESC O
// Single Shift Select of G3 Character Set
// ( SS3 is 0x8f). This affects next character only.
case 'O':
this._state = ParserState.NORMAL;
break;
// ESC n
// Invoke the G2 Character Set as GL (LS2).
case 'n':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC o
// Invoke the G3 Character Set as GL (LS3).
case 'o':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC |
// Invoke the G3 Character Set as GR (LS3R).
case '|':
this._terminal.setgLevel(3);
this._state = ParserState.NORMAL;
break;
// ESC }
// Invoke the G2 Character Set as GR (LS2R).
case '}':
this._terminal.setgLevel(2);
this._state = ParserState.NORMAL;
break;
// ESC ~
// Invoke the G1 Character Set as GR (LS1R).
case '~':
this._terminal.setgLevel(1);
this._state = ParserState.NORMAL;
break;
// ESC 7 Save Cursor (DECSC).
@@ -338,7 +345,7 @@ export class Parser {
// ESC H Tab Set (HTS is 0x88).
case 'H':
this._terminal.tabSet();
(<IInputHandlingTerminal>this._terminal).tabSet();
this._state = ParserState.NORMAL;
break;
+43 -16
View File
@@ -3,16 +3,13 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ITerminal, ICircularList, IBuffer } from './Interfaces';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { SelectionManager } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
import { LineData, CharData, ITerminal, IBuffer } from './Types';
import { MockTerminal } from './utils/TestUtils.test';
import { LineData, CharData } from './Types';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
@@ -21,16 +18,15 @@ class TestMockTerminal extends MockTerminal {
class TestSelectionManager extends SelectionManager {
constructor(
terminal: ITerminal,
buffer: IBuffer,
charMeasure: CharMeasure
) {
super(terminal, buffer, charMeasure);
super(terminal, charMeasure);
}
public get model(): SelectionModel { return this._model; }
public selectLineAt(line: number): void { this._selectLineAt(line); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords); }
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
// Disable DOM interaction
public enable(): void {}
@@ -39,19 +35,11 @@ class TestSelectionManager extends SelectionManager {
}
describe('SelectionManager', () => {
let dom: jsdom.JSDOM;
let window: Window;
let document: Document;
let terminal: ITerminal;
let buffer: IBuffer;
let rowContainer: HTMLElement;
let selectionManager: TestSelectionManager;
beforeEach(() => {
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
terminal = new TestMockTerminal();
terminal.cols = 80;
terminal.rows = 2;
@@ -59,7 +47,7 @@ describe('SelectionManager', () => {
terminal.buffers = new BufferSet(terminal);
terminal.buffer = terminal.buffers.active;
buffer = terminal.buffer;
selectionManager = new TestSelectionManager(terminal, buffer, null);
selectionManager = new TestSelectionManager(terminal, null);
});
function stringToRow(text: string): LineData {
@@ -326,6 +314,45 @@ describe('SelectionManager', () => {
});
});
describe('selectLines', () => {
it('should select a single line', () => {
buffer.lines.length = 3;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
buffer.lines.set(2, stringToRow('3'));
selectionManager.selectLines(1, 1);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
});
it('should select multiple lines', () => {
buffer.lines.length = 5;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
buffer.lines.set(2, stringToRow('3'));
buffer.lines.set(3, stringToRow('4'));
buffer.lines.set(4, stringToRow('5'));
selectionManager.selectLines(1, 3);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]);
});
it('should select the to the start when requesting a negative row', () => {
buffer.lines.length = 2;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
selectionManager.selectLines(-1, 0);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]);
});
it('should select the to the end when requesting beyond the final row', () => {
buffer.lines.length = 2;
buffer.lines.set(0, stringToRow('1'));
buffer.lines.set(1, stringToRow('2'));
selectionManager.selectLines(1, 2);
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
});
});
describe('hasSelection', () => {
it('should return whether there is a selection', () => {
selectionManager.model.selectionStart = [0, 0];
+100 -30
View File
@@ -3,15 +3,14 @@
* @license MIT
*/
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
import { MouseHelper } from './utils/MouseHelper';
import * as Browser from './utils/Browser';
import * as Browser from './shared/utils/Browser';
import { CharMeasure } from './utils/CharMeasure';
import { CircularList } from './utils/CircularList';
import { EventEmitter } from './EventEmitter';
import { ITerminal, ICircularList, ISelectionManager, IBuffer } from './Interfaces';
import { SelectionModel } from './SelectionModel';
import { LineData, CharData } from './Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
import { AltClickHandler } from './handlers/AltClickHandler';
/**
* The number of pixels the mouse needs to be above or below the viewport in
@@ -29,6 +28,12 @@ const DRAG_SCROLL_MAX_SPEED = 15;
*/
const DRAG_SCROLL_INTERVAL = 50;
/**
* The maximum amount of time that can have elapsed for an alt click to move the
* cursor.
*/
const ALT_CLICK_MOVE_CURSOR_TIME = 500;
/**
* A string containing all characters that are considered word separated by the
* double click to select work logic.
@@ -49,7 +54,7 @@ interface IWordPosition {
/**
* A selection mode, this drives how the selection behaves on mouse move.
*/
enum SelectionMode {
const enum SelectionMode {
NORMAL,
WORD,
LINE
@@ -95,10 +100,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
private _mouseMoveListener: EventListener;
private _mouseUpListener: EventListener;
private _trimListener: XtermListener;
private _mouseDownTimeStamp: number;
constructor(
private _terminal: ITerminal,
private _buffer: IBuffer,
private _charMeasure: CharMeasure
) {
super();
@@ -109,18 +116,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._activeSelectionMode = SelectionMode.NORMAL;
}
private get _buffer(): IBuffer {
return this._terminal.buffers.active;
}
/**
* Initializes listener variables.
*/
private _initListeners(): void {
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
this._trimListener = (amount: number) => this._onTrim(amount);
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
this._buffer.lines.on('trim', (amount: number) => this._onTrim(amount));
this.initBuffersListeners();
}
public initBuffersListeners(): void {
this._terminal.buffer.lines.on('trim', this._trimListener);
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
}
/**
@@ -139,16 +152,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._enabled = true;
}
/**
* Sets the active buffer, this should be called when the alt buffer is
* switched in or out.
* @param buffer The active buffer.
*/
public setBuffer(buffer: IBuffer): void {
this._buffer = buffer;
this.clearSelection();
}
public get selectionStart(): [number, number] { return this._model.finalSelectionStart; }
public get selectionEnd(): [number, number] { return this._model.finalSelectionEnd; }
@@ -249,6 +252,37 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
}
/**
* Checks if the current click was inside the current selection
* @param event The mouse event
*/
public isClickInSelection(event: MouseEvent): boolean {
const coords = this._getMouseBufferCoords(event);
const start = this._model.finalSelectionStart;
const end = this._model.finalSelectionEnd;
if (!start || !end) {
return false;
}
return (coords[1] > start[1] && coords[1] < end[1]) ||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
}
/**
* Selects word at the current mouse event coordinates.
* @param event The mouse event.
*/
public selectWordAtCursor(event: MouseEvent): void {
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._selectWordAt(coords, false);
this._model.selectionEnd = null;
this.refresh(true);
}
}
/**
* Selects all text within the terminal.
*/
@@ -258,6 +292,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.emit('selection');
}
public selectLines(start: number, end: number): void {
this._model.clearSelection();
start = Math.max(start, 0);
end = Math.min(end, this._terminal.buffer.lines.length - 1);
this._model.selectionStart = [0, start];
this._model.selectionEnd = [this._terminal.cols, end];
this.refresh();
this._terminal.emit('selection');
}
/**
* Handle the buffer being trimmed, adjust the selection position.
* @param amount The amount the buffer is being trimmed.
@@ -274,7 +318,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.element, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
if (!coords) {
return null;
}
@@ -294,7 +338,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouse event.
*/
private _getMouseEventScrollAmount(event: MouseEvent): number {
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.element)[1];
let offset = MouseHelper.getCoordsRelativeToElement(event, this._terminal.screenElement)[1];
const terminalHeight = this._terminal.rows * Math.ceil(this._charMeasure.height * this._terminal.options.lineHeight);
if (offset >= 0 && offset <= terminalHeight) {
return 0;
@@ -322,6 +366,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mousedown event.
*/
public onMouseDown(event: MouseEvent): void {
this._mouseDownTimeStamp = event.timeStamp;
// If we have selection, we want the context menu on right click even if the
// terminal is in mouse mode.
if (event.button === 2 && this.hasSelection) {
@@ -440,7 +485,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
const coords = this._getMouseBufferCoords(event);
if (coords) {
this._activeSelectionMode = SelectionMode.WORD;
this._selectWordAt(coords);
this._selectWordAt(coords, true);
}
}
@@ -528,7 +573,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
this._terminal.scrollLines(this._dragScrollAmount, false);
// Re-evaluate selection
if (this._dragScrollAmount > 0) {
this._model.selectionEnd = [this._terminal.cols - 1, this._terminal.buffer.ydisp + this._terminal.rows];
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
} else {
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
}
@@ -541,10 +586,25 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param event The mouseup event.
*/
private _onMouseUp(event: MouseEvent): void {
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
this._removeMouseDownListeners();
if (this.hasSelection)
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
(new AltClickHandler(event, this._terminal)).move();
} else if (this.hasSelection) {
this._terminal.emit('selection');
}
}
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
this.clearSelection();
// Only adjust the selection on trim, shiftElements is rarely used (only in
// reverseIndex) and delete in a splice is only ever used when the same
// number of elements was just added. Given this is could actually be
// beneficial to leave the selection as is for these cases.
e.inactiveBuffer.lines.off('trim', this._trimListener);
e.activeBuffer.lines.on('trim', this._trimListener);
}
/**
@@ -582,7 +642,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* Gets positional information for the word at the coordinated specified.
* @param coords The coordinates to get the word at.
*/
private _getWordAt(coords: [number, number]): IWordPosition {
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
// Ensure coords are within viewport (eg. not within scroll bar)
if (coords[0] >= this._terminal.cols) {
return null;
}
const bufferLine = this._buffer.lines.get(coords[1]);
if (!bufferLine) {
return null;
@@ -688,15 +753,20 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
- leftLongCharOffset // The number of additional chars left of the initial char added by columns with strings longer than 1 (emojis)
- rightLongCharOffset); // The number of additional chars right of the initial char (inclusive) added by columns with strings longer than 1 (emojis)
if (!allowWhitespaceOnlySelection && line.slice(startIndex, endIndex).trim() === '') {
return null;
}
return { start, length };
}
/**
* Selects the word at the coordinates specified.
* @param coords The coordinates to get the word at.
* @param allowWhitespaceOnlySelection If whitespace should be selected
*/
protected _selectWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
if (wordPosition) {
this._model.selectionStart = [wordPosition.start, coords[1]];
this._model.selectionStartLength = wordPosition.length;
@@ -708,7 +778,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
* @param coords The coordinates to get the word at.
*/
private _selectToWordAt(coords: [number, number]): void {
const wordPosition = this._getWordAt(coords);
const wordPosition = this._getWordAt(coords, true);
if (wordPosition) {
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
}
+2 -5
View File
@@ -4,9 +4,9 @@
*/
import { assert } from 'chai';
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
import { SelectionModel } from './SelectionModel';
import {BufferSet} from './BufferSet';
import { BufferSet } from './BufferSet';
import { MockTerminal } from './utils/TestUtils.test';
class TestSelectionModel extends SelectionModel {
@@ -18,9 +18,6 @@ class TestSelectionModel extends SelectionModel {
}
describe('SelectionManager', () => {
let window: Window;
let document: Document;
let terminal: ITerminal;
let model: TestSelectionModel;
+1 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal } from './Interfaces';
import { ITerminal } from './Types';
/**
* Represents a selection within the buffer. This model only cares about column
+60
View File
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ISoundManager } from './Types';
// Source: https://freesound.org/people/altemark/sounds/45759/
// This sound is released under the Creative Commons Attribution 3.0 Unported
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
// made, apart from the conversion to base64.
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
export class SoundManager implements ISoundManager {
private _audioContext: AudioContext;
constructor(
private _terminal: ITerminal
) {
}
public playBellSound(): void {
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
if (!this._audioContext && audioContextCtor) {
this._audioContext = new audioContextCtor();
}
if (this._audioContext) {
const bellAudioSource = this._audioContext.createBufferSource();
const context = this._audioContext;
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
bellAudioSource.buffer = buffer;
bellAudioSource.connect(context.destination);
bellAudioSource.start(0);
});
} else {
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
}
}
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
const binaryString = window.atob(base64);
const len = binaryString.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
}
private _removeMimeType(dataURI: string): string {
// Split the input to get the mime-type and the data itself
const splitUri = dataURI.split(',');
// Return only the data
return splitUri[1];
}
}
+8
View File
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
export let blankLine = 'Blank line';
export let promptLabel = 'Terminal input';
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
+19 -19
View File
@@ -15,17 +15,17 @@ import { assert } from 'chai';
import { Terminal } from './Terminal';
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
let primitive_pty: any;
let primitivePty: any;
// fake sychronous pty write - read
// we just pipe the data from slave to master as a child program would do
// pty.js opens pipe fds with O_NONBLOCK
// just wait 10ms instead of setting fds to blocking mode
function ptyWriteRead(data: string, cb: (result: string) => void): void {
fs.writeSync(primitive_pty.slave, data);
fs.writeSync(primitivePty.slave, data);
setTimeout(() => {
let b = new Buffer(64000);
let bytes = fs.readSync(primitive_pty.master, b, 0, 64000, null);
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
cb(b.toString('utf8', 0, bytes));
});
}
@@ -37,7 +37,7 @@ function ptyReset(cb: (result: string) => void): void {
/* debug helpers */
// generate colorful noisy output to compare xterm and emulator cell states
function formatError(in_: string, out_: string, expected: string): string {
function formatError(input: string, output: string, expected: string): string {
function addLineNumber(start: number, color: string): (s: string) => string {
let counter = start || 0;
return function(s: string): string {
@@ -47,9 +47,9 @@ function formatError(in_: string, out_: string, expected: string): string {
}
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
let s = '';
s += '\n\x1b[34m' + JSON.stringify(in_);
s += '\n\x1b[34m' + JSON.stringify(input);
s += '\n\x1b[33m ' + line80 + '\n';
s += out_.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += output.split('\n').map(addLineNumber(0, '\x1b[31m')).join('\n');
s += '\n\x1b[33m ' + line80 + '\n';
s += expected.split('\n').map(addLineNumber(0, '\x1b[32m')).join('\n');
return s;
@@ -58,15 +58,15 @@ function formatError(in_: string, out_: string, expected: string): string {
// simple debug output of terminal cells
function terminalToString(term: Terminal): string {
let result = '';
let line_s = '';
let lineText = '';
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
line_s = '';
lineText = '';
for (let cell = 0; cell < term.cols; ++cell) {
line_s += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
}
// rtrim empty cells as xterm does
line_s = line_s.replace(/\s+$/, '');
result += line_s;
lineText = lineText.replace(/\s+$/, '');
result += lineText;
result += '\n';
}
return result;
@@ -83,7 +83,7 @@ if (os.platform() !== 'win32') {
/** some helpers for pty interaction */
// we need a pty in between to get the termios decorations
// for the basic test cases a raw pty device is enough
primitive_pty = pty.native.open(COLS, ROWS);
primitivePty = pty.native.open(COLS, ROWS);
/** tests */
describe('xterm output comparison', () => {
@@ -118,24 +118,24 @@ if (os.platform() !== 'win32') {
((filename: string) => {
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
let in_file = fs.readFileSync(filename, 'utf8');
ptyWriteRead(in_file, from_pty => {
let inFile = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
// Perform a synchronous .write(data)
xterm.writeBuffer.push(from_pty);
xterm.innerWrite();
xterm.writeBuffer.push(fromPty);
xterm._innerWrite();
let from_emulator = terminalToString(xterm);
let fromEmulator = terminalToString(xterm);
console.log = CONSOLE_LOG;
let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
// Some of the tests have whitespace on the right of lines, we trim all the linex
// from xterm.js so ignore this for now at least.
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
if (from_emulator !== expectedRightTrimmed) {
if (fromEmulator !== expectedRightTrimmed) {
// uncomment to get noisy output
throw new Error(formatError(in_file, from_emulator, expected));
throw new Error(formatError(inFile, fromEmulator, expected));
// throw new Error('mismatch');
}
done();
+169 -6
View File
@@ -20,20 +20,21 @@ class TestTerminal extends Terminal {
describe('term.js addons', () => {
let term: TestTerminal;
const termOptions = {
cols: INIT_COLS,
rows: INIT_ROWS
};
beforeEach(() => {
term = new TestTerminal({
cols: INIT_COLS,
rows: INIT_ROWS
});
term = new TestTerminal(termOptions);
term.refresh = () => {};
(<any>term).renderer = new MockRenderer();
term.viewport = new MockViewport();
(<any>term).compositionHelper = new MockCompositionHelper();
(<any>term)._compositionHelper = new MockCompositionHelper();
// Force synchronous writes
term.write = (data) => {
term.writeBuffer.push(data);
(<any>term).innerWrite();
(<any>term)._innerWrite();
};
(<any>term).element = {
classList: {
@@ -43,6 +44,15 @@ describe('term.js addons', () => {
};
});
it('should not mutate the options parameter', () => {
term.setOption('cols', 1000);
assert.deepEqual(termOptions, {
cols: INIT_COLS,
rows: INIT_ROWS
});
});
it('should apply addons with Terminal.applyAddon', () => {
Terminal.applyAddon(attach);
// Test that addon was applied successfully, adding attach to Terminal's
@@ -258,6 +268,34 @@ describe('term.js addons', () => {
});
});
describe('scrollToLine', () => {
let startYDisp;
beforeEach(() => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeln('test');
}
startYDisp = (term.rows * 2) + 1;
});
it('should scroll to requested line', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(0);
assert.equal(term.buffer.ydisp, 0);
term.scrollToLine(10);
assert.equal(term.buffer.ydisp, 10);
term.scrollToLine(startYDisp);
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(20);
assert.equal(term.buffer.ydisp, 20);
});
it('should not scroll beyond boundary lines', () => {
assert.equal(term.buffer.ydisp, startYDisp);
term.scrollToLine(-1);
assert.equal(term.buffer.ydisp, 0);
term.scrollToLine(startYDisp + 1);
assert.equal(term.buffer.ydisp, startYDisp);
});
});
describe('keyDown', () => {
it('should scroll down, when a key is pressed and terminal is scrolled up', () => {
// Override _evaluateKeyEscapeSequence to return cancel code
@@ -503,6 +541,9 @@ describe('term.js addons', () => {
it('should return \\x1b[5C for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
@@ -515,6 +556,19 @@ describe('term.js addons', () => {
it('should return \\x1bf for alt+right', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
});
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should return \\x1ba for alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
@@ -562,6 +616,99 @@ describe('term.js addons', () => {
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
});
describe('Third level shift', () => {
@@ -597,6 +744,22 @@ describe('term.js addons', () => {
};
});
describe('with macOptionIsMeta', () => {
beforeEach(() => {
term.browser.isMac = true;
term.setOption('macOptionIsMeta', true);
});
it('should interfere with the alt key on keyDown', () => {
evKeyDown.altKey = true;
evKeyDown.keyCode = 81;
assert.equal(term.keyDown(evKeyDown), false);
evKeyDown.altKey = true;
evKeyDown.keyCode = 192;
assert.equal(term.keyDown(evKeyDown), false);
});
});
describe('On Mac OS', () => {
beforeEach(() => {
term.browser.isMac = true;
+411 -360
View File
File diff suppressed because it is too large Load Diff
+342 -20
View File
@@ -3,33 +3,355 @@
* @license MIT
*/
export type LinkMatcher = {
id: number,
regex: RegExp,
handler: LinkMatcherHandler,
hoverTooltipCallback?: LinkMatcherHandler,
hoverLeaveCallback?: () => void,
matchIndex?: number,
validationCallback?: LinkMatcherValidationCallback,
priority?: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
import { IMouseZoneManager } from './input/Types';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
export type Charset = {[key: string]: string};
export type XtermListener = (...args: any[]) => void;
export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkHoverEvent = {
x: number,
y: number,
length: number
};
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export enum LinkHoverEventTypes {
export const enum LinkHoverEventTypes {
HOVER = 'linkhover',
TOOLTIP = 'linktooltip',
LEAVE = 'linkleave'
};
}
/**
* This interface encapsulates everything needed from the Terminal by the
* InputHandler. This cleanly separates the large amount of methods needed by
* InputHandler cleanly from the ITerminal interface.
*/
export interface IInputHandlingTerminal extends IEventEmitter {
element: HTMLElement;
options: ITerminalOptions;
cols: number;
rows: number;
charset: ICharset;
gcharset: number;
glevel: number;
charsets: ICharset[];
applicationKeypad: boolean;
applicationCursor: boolean;
originMode: boolean;
insertMode: boolean;
wraparoundMode: boolean;
bracketedPasteMode: boolean;
defAttr: number;
curAttr: number;
prefix: string;
savedCols: number;
x10Mouse: boolean;
vt200Mouse: boolean;
normalMouse: boolean;
mouseEvents: boolean;
sendFocus: boolean;
utfMouse: boolean;
sgrMouse: boolean;
urxvtMouse: boolean;
cursorHidden: boolean;
buffers: IBufferSet;
buffer: IBuffer;
viewport: IViewport;
selectionManager: ISelectionManager;
bell(): void;
focus(): void;
convertEol: boolean;
updateRange(y: number): void;
scroll(isWrapped?: boolean): void;
setgLevel(g: number): void;
eraseAttr(): number;
eraseRight(x: number, y: number): void;
eraseLine(y: number): void;
eraseLeft(x: number, y: number): void;
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
is(term: string): boolean;
send(data: string): void;
setgCharset(g: number, charset: ICharset): void;
resize(x: number, y: number): void;
log(text: string, data?: any): void;
reset(): void;
showCursor(): void;
refresh(start: number, end: number): void;
matchColor(r1: number, g1: number, b1: number): number;
error(text: string, data?: any): void;
setOption(key: string, value: any): void;
tabSet(): void;
}
export interface IViewport {
scrollBarWidth: number;
syncScrollArea(): void;
getLinesScrolled(ev: WheelEvent): number;
onWheel(ev: WheelEvent): void;
onTouchStart(ev: TouchEvent): void;
onTouchMove(ev: TouchEvent): void;
onThemeChanged(colors: IColorSet): void;
}
export interface ICompositionHelper {
compositionstart(): void;
compositionupdate(ev: CompositionEvent): void;
compositionend(): void;
updateCompositionElements(dontRecurse?: boolean): void;
keydown(ev: KeyboardEvent): boolean;
}
/**
* Handles actions generated by the parser.
*/
export interface IInputHandler {
addChar(char: string, code: number): void;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
/** C0 CR */ carriageReturn(): void;
/** C0 BS */ backspace(): void;
/** C0 HT */ tab(): void;
/** C0 SO */ shiftOut(): void;
/** C0 SI */ shiftIn(): void;
/** CSI @ */ insertChars(params?: number[]): void;
/** CSI A */ cursorUp(params?: number[]): void;
/** CSI B */ cursorDown(params?: number[]): void;
/** CSI C */ cursorForward(params?: number[]): void;
/** CSI D */ cursorBackward(params?: number[]): void;
/** CSI E */ cursorNextLine(params?: number[]): void;
/** CSI F */ cursorPrecedingLine(params?: number[]): void;
/** CSI G */ cursorCharAbsolute(params?: number[]): void;
/** CSI H */ cursorPosition(params?: number[]): void;
/** CSI I */ cursorForwardTab(params?: number[]): void;
/** CSI J */ eraseInDisplay(params?: number[]): void;
/** CSI K */ eraseInLine(params?: number[]): void;
/** CSI L */ insertLines(params?: number[]): void;
/** CSI M */ deleteLines(params?: number[]): void;
/** CSI P */ deleteChars(params?: number[]): void;
/** CSI S */ scrollUp(params?: number[]): void;
/** CSI T */ scrollDown(params?: number[]): void;
/** CSI X */ eraseChars(params?: number[]): void;
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
/** CSI ` */ charPosAbsolute(params?: number[]): void;
/** CSI a */ HPositionRelative(params?: number[]): void;
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
/** CSI d */ linePosAbsolute(params?: number[]): void;
/** CSI e */ VPositionRelative(params?: number[]): void;
/** CSI f */ HVPosition(params?: number[]): void;
/** CSI g */ tabClear(params?: number[]): void;
/** CSI h */ setMode(params?: number[]): void;
/** CSI l */ resetMode(params?: number[]): void;
/** CSI m */ charAttributes(params?: number[]): void;
/** CSI n */ deviceStatus(params?: number[]): void;
/** CSI p */ softReset(params?: number[]): void;
/** CSI q */ setCursorStyle(params?: number[]): void;
/** CSI r */ setScrollRegion(params?: number[]): void;
/** CSI s */ saveCursor(params?: number[]): void;
/** CSI u */ restoreCursor(params?: number[]): void;
}
export interface ILinkMatcher {
id: number;
regex: RegExp;
handler: LinkMatcherHandler;
hoverTooltipCallback?: LinkMatcherHandler;
hoverLeaveCallback?: () => void;
matchIndex?: number;
validationCallback?: LinkMatcherValidationCallback;
priority?: number;
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface ICharset {
[key: string]: string;
}
export interface ILinkHoverEvent {
x1: number;
y1: number;
x2: number;
y2: number;
cols: number;
}
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
renderer: IRenderer;
browser: IBrowser;
writeBuffer: string[];
cursorHidden: boolean;
cursorState: number;
defAttr: number;
options: ITerminalOptions;
buffer: IBuffer;
buffers: IBufferSet;
isFocused: boolean;
mouseHelper: IMouseHelper;
viewport: IViewport;
bracketedPasteMode: boolean;
applicationCursor: boolean;
/**
* Emit the 'data' event and populate the given data.
* @param data The data to populate in the event.
*/
handler(data: string): void;
send(data: string): void;
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
cancel(ev: Event, force?: boolean): boolean | void;
log(text: string): void;
showCursor(): void;
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
}
export interface IBufferAccessor {
buffer: IBuffer;
}
export interface IElementAccessor {
element: HTMLElement;
}
export interface ILinkifierAccessor {
linkifier: ILinkifier;
}
export interface IMouseHelper {
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
}
export interface ICharMeasure {
width: number;
height: number;
measure(options: ITerminalOptions): void;
}
// TODO: The options that are not in the public API should be reviewed
export interface ITerminalOptions extends IPublicTerminalOptions {
cancelEvents?: boolean;
convertEol?: boolean;
debug?: boolean;
handler?: (data: string) => void;
screenKeys?: boolean;
termName?: string;
useFlowControl?: boolean;
}
export interface IBuffer {
readonly lines: ICircularList<LineData>;
ydisp: number;
ybase: number;
y: number;
x: number;
tabs: any;
scrollBottom: number;
scrollTop: number;
hasScrollback: boolean;
savedY: number;
savedX: number;
isCursorInViewport: boolean;
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
nextStop(x?: number): number;
prevStop(x?: number): number;
}
export interface IBufferSet extends IEventEmitter {
alt: IBuffer;
normal: IBuffer;
active: IBuffer;
activateNormalBuffer(): void;
activateAltBuffer(): void;
}
export interface ICircularList<T> extends IEventEmitter {
length: number;
maxLength: number;
get(index: number): T;
set(index: number, value: T): void;
push(value: T): void;
pop(): T;
splice(start: number, deleteCount: number, ...items: T[]): void;
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export interface ISelectionManager {
selectionText: string;
selectionStart: [number, number];
selectionEnd: [number, number];
disable(): void;
enable(): void;
setSelection(row: number, col: number, length: number): void;
isClickInSelection(event: MouseEvent): boolean;
selectWordAtCursor(event: MouseEvent): void;
}
export interface ILinkifier extends IEventEmitter {
attachToDom(mouseZoneManager: IMouseZoneManager): void;
linkifyRows(start: number, end: number): void;
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): boolean;
}
export interface ILinkMatcherOptions {
/**
* The index of the link from the regex.match(text) call. This defaults to 0
* (for regular expressions without capture groups).
*/
matchIndex?: number;
/**
* A callback that validates an individual link, returning true if valid and
* false if invalid.
*/
validationCallback?: LinkMatcherValidationCallback;
/**
* A callback that fires when the mouse hovers over a link.
*/
tooltipCallback?: LinkMatcherHandler;
/**
* A callback that fires when the mouse leaves a link that was hovered.
*/
leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
* matcher is evaluated relative to others, from highest to lowest. The
* default value is 0.
*/
priority?: number;
/**
* A callback that fires when the mousedown and click events occur that
* determines whether a link will be activated upon click. This enables
* only activating a link when a certain modifier is held down, if not the
* mouse event will continue propagation (eg. double click to select word).
*/
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
}
export interface IBrowser {
isNode: boolean;
userAgent: string;
platform: string;
isFirefox: boolean;
isMSIE: boolean;
isMac: boolean;
isIpad: boolean;
isIphone: boolean;
isMSWindows: boolean;
}
export interface ISoundManager {
playBellSound(): void;
}
+107 -61
View File
@@ -3,61 +3,68 @@
* @license MIT
*/
import { ITerminal, IViewport } from './Interfaces';
import { IColorSet } from './renderer/Types';
import { ITerminal, IViewport } from './Types';
import { CharMeasure } from './utils/CharMeasure';
import { IColorSet } from './renderer/Interfaces';
const FALLBACK_SCROLL_BAR_WIDTH = 15;
/**
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
* Logic for the virtual scroll bar is included in this object.
*/
export class Viewport implements IViewport {
private currentRowHeight: number = 0;
private lastRecordedBufferLength: number = 0;
private lastRecordedViewportHeight: number = 0;
private lastRecordedBufferHeight: number = 0;
private lastTouchY: number;
public scrollBarWidth: number = 0;
private _currentRowHeight: number = 0;
private _lastRecordedBufferLength: number = 0;
private _lastRecordedViewportHeight: number = 0;
private _lastRecordedBufferHeight: number = 0;
private _lastTouchY: number;
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
// quick fix and could have a more robust solution in place that reset the value when needed.
private _wheelPartialScroll: number = 0;
/**
* Creates a new Viewport.
* @param terminal The terminal this viewport belongs to.
* @param viewportElement The DOM element acting as the viewport.
* @param scrollArea The DOM element acting as the scroll area.
* @param charMeasure A DOM element used to measure the character size of. the terminal.
* @param _terminal The terminal this viewport belongs to.
* @param _viewportElement The DOM element acting as the viewport.
* @param _scrollArea The DOM element acting as the scroll area.
* @param _charMeasure A DOM element used to measure the character size of. the terminal.
*/
constructor(
private terminal: ITerminal,
private viewportElement: HTMLElement,
private scrollArea: HTMLElement,
private charMeasure: CharMeasure
private _terminal: ITerminal,
private _viewportElement: HTMLElement,
private _scrollArea: HTMLElement,
private _charMeasure: CharMeasure
) {
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
// therefore we account for a standard amount to make it visible
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
this._viewportElement.addEventListener('scroll', this._onScroll.bind(this));
// Perform this async to ensure the CharMeasure is ready.
setTimeout(() => this.syncScrollArea(), 0);
}
public onThemeChanged(colors: IColorSet): void {
this.viewportElement.style.backgroundColor = colors.background;
this._viewportElement.style.backgroundColor = colors.background.css;
}
/**
* Refreshes row height, setting line-height, viewport height and scroll area height if
* necessary.
*/
private refresh(): void {
if (this.charMeasure.height > 0) {
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
if (this.lastRecordedViewportHeight !== this.terminal.renderer.dimensions.canvasHeight) {
this.lastRecordedViewportHeight = this.terminal.renderer.dimensions.canvasHeight;
this.viewportElement.style.height = this.lastRecordedViewportHeight + 'px';
}
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength);
if (this.lastRecordedBufferHeight !== newBufferHeight) {
this.lastRecordedBufferHeight = newBufferHeight;
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
private _refresh(): void {
if (this._charMeasure.height > 0) {
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
if (this._lastRecordedBufferHeight !== newBufferHeight) {
this._lastRecordedBufferHeight = newBufferHeight;
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
}
}
}
@@ -66,24 +73,24 @@ export class Viewport implements IViewport {
* Updates dimensions and synchronizes the scroll area if necessary.
*/
public syncScrollArea(): void {
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
// If buffer height changed
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
this.refresh();
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
this._refresh();
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
// If viewport height changed
this.refresh();
this._refresh();
} else {
// If size has changed, refresh viewport
if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) {
this.refresh();
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
this._refresh();
}
}
// Sync scrollTop
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
if (this.viewportElement.scrollTop !== scrollTop) {
this.viewportElement.scrollTop = scrollTop;
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
if (this._viewportElement.scrollTop !== scrollTop) {
this._viewportElement.scrollTop = scrollTop;
}
}
@@ -92,10 +99,16 @@ export class Viewport implements IViewport {
* terminal to scroll to it.
* @param ev The scroll event.
*/
private onScroll(ev: Event): void {
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
const diff = newRow - this.terminal.buffer.ydisp;
this.terminal.scrollLines(diff, true);
private _onScroll(ev: Event): void {
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
// which causes the terminal to scroll the buffer to the top
if (!this._viewportElement.offsetParent) {
return;
}
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
const diff = newRow - this._terminal.buffer.ydisp;
this._terminal.scrollLines(diff, true);
}
/**
@@ -105,41 +118,74 @@ export class Viewport implements IViewport {
* @param ev The mouse wheel event.
*/
public onWheel(ev: WheelEvent): void {
if (ev.deltaY === 0) {
// Do nothing if it's not a vertical scroll event
const amount = this._getPixelsScrolled(ev);
if (amount === 0) {
return;
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let multiplier = 1;
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
multiplier = this.currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
multiplier = this.currentRowHeight * this.terminal.rows;
}
this.viewportElement.scrollTop += ev.deltaY * multiplier;
this._viewportElement.scrollTop += amount;
// Prevent the page from scrolling when the terminal scrolls
ev.preventDefault();
};
}
private _getPixelsScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_PIXEL
let amount = ev.deltaY;
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
amount *= this._currentRowHeight;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._currentRowHeight * this._terminal.rows;
}
return amount;
}
/**
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
* is being used.
* @param ev The mouse wheel event.
*/
public getLinesScrolled(ev: WheelEvent): number {
// Do nothing if it's not a vertical scroll event
if (ev.deltaY === 0) {
return 0;
}
// Fallback to WheelEvent.DOM_DELTA_LINE
let amount = ev.deltaY;
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
amount /= this._currentRowHeight + 0.0; // Prevent integer division
this._wheelPartialScroll += amount;
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
this._wheelPartialScroll %= 1;
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
amount *= this._terminal.rows;
}
return amount;
}
/**
* Handles the touchstart event, recording the touch occurred.
* @param ev The touch event.
*/
public onTouchStart(ev: TouchEvent): void {
this.lastTouchY = ev.touches[0].pageY;
};
this._lastTouchY = ev.touches[0].pageY;
}
/**
* Handles the touchmove event, scrolling the viewport if the position shifted.
* @param ev The touch event.
*/
public onTouchMove(ev: TouchEvent): void {
let deltaY = this.lastTouchY - ev.touches[0].pageY;
this.lastTouchY = ev.touches[0].pageY;
let deltaY = this._lastTouchY - ev.touches[0].pageY;
this._lastTouchY = ev.touches[0].pageY;
if (deltaY === 0) {
return;
}
this.viewportElement.scrollTop += deltaY;
this._viewportElement.scrollTop += deltaY;
ev.preventDefault();
};
}
}
+18
View File
@@ -0,0 +1,18 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface IAttachAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as attach from './attach'
import * as attach from './attach';
class MockTerminal {}
describe('attach addon', () => {
describe('apply', () => {
it('should do register the `attach` and `detach` methods', () => {
attach.apply(MockTerminal);
attach.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.attach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.detach, 'function');
});
+82 -62
View File
@@ -5,118 +5,138 @@
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IAttachAddonTerminal } from './Interfaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function attach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <IAttachAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
var myTextDecoder;
let myTextDecoder;
term._getMessage = function(ev) {
var str;
if (typeof ev.data === "object") {
addonTerminal.__getMessage = function(ev: MessageEvent): void {
let str;
if (typeof ev.data === 'object') {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
if (ev.data instanceof ArrayBuffer) {
if (!myTextDecoder) {
myTextDecoder = new TextDecoder();
}
str = myTextDecoder.decode(ev.data);
displayData(str);
} else {
let fileReader = new FileReader();
str = myTextDecoder.decode( ev.data );
fileReader.addEventListener('load', () => {
str = myTextDecoder.decode(this.result);
displayData(str);
});
fileReader.readAsArrayBuffer(ev.data);
}
else {
throw "TODO: handle Blob?";
}
}
if (buffered) {
term._pushToBuffer(str || ev.data);
} else if (typeof ev.data === 'string') {
displayData(ev.data);
} else {
term.write(str || ev.data);
throw Error(`Cannot handle "${typeof ev.data}" websocket message.`);
}
};
term._sendData = function(data) {
/**
* Push data to buffer or write it in the terminal.
* This is used as a callback for FileReader.onload.
*
* @param str String decoded by FileReader.
* @param data The data of the EventMessage.
*/
function displayData(str?: string, data?: string): void {
if (buffered) {
addonTerminal.__pushToBuffer(str || data);
} else {
addonTerminal.write(str || data);
}
}
addonTerminal.__sendData = (data: string) => {
if (socket.readyState !== 1) {
return;
}
socket.send(data);
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
socket.addEventListener('close', term.detach.bind(term, socket));
socket.addEventListener('error', term.detach.bind(term, socket));
};
socket.addEventListener('close', () => detach(addonTerminal, socket));
socket.addEventListener('error', () => detach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Terminal} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function detach(term, socket) {
term.off('data', term._sendData);
export function detach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <IAttachAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.attach = function(socket, bidirectional, buffered) {
return attach(this, socket, bidirectional, buffered);
(<any>terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
attach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.detach = function(socket) {
return detach(this, socket);
(<any>terminalConstructor.prototype).detach = function (socket: WebSocket): void {
detach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/attach/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fit from './fit'
import * as fit from './fit';
class MockTerminal {}
describe('fit addon', () => {
describe('apply', () => {
it('should do register the `proposeGeometry` and `fit` methods', () => {
fit.apply(MockTerminal);
fit.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.proposeGeometry, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.fit, 'function');
});
+41 -27
View File
@@ -13,43 +13,57 @@
* row and truncate its width with the current number of columns).
*/
export function proposeGeometry(term) {
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
export interface IGeometry {
rows: number;
cols: number;
}
export function proposeGeometry(term: Terminal): IGeometry {
if (!term.element.parentElement) {
return null;
}
var parentElementStyle = window.getComputedStyle(term.element.parentElement);
var parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
var parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')) - 17);
var elementStyle = window.getComputedStyle(term.element);
var elementPaddingVer = parseInt(elementStyle.getPropertyValue('padding-top')) + parseInt(elementStyle.getPropertyValue('padding-bottom'));
var elementPaddingHor = parseInt(elementStyle.getPropertyValue('padding-right')) + parseInt(elementStyle.getPropertyValue('padding-left'));
var availableHeight = parentElementHeight - elementPaddingVer;
var availableWidth = parentElementWidth - elementPaddingHor;
var geometry = {
cols: Math.floor(availableWidth / term.renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / term.renderer.dimensions.actualCellHeight)
const parentElementStyle = window.getComputedStyle(term.element.parentElement);
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
const elementStyle = window.getComputedStyle(term.element);
const elementPadding = {
top: parseInt(elementStyle.getPropertyValue('padding-top')),
bottom: parseInt(elementStyle.getPropertyValue('padding-bottom')),
right: parseInt(elementStyle.getPropertyValue('padding-right')),
left: parseInt(elementStyle.getPropertyValue('padding-left'))
};
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
const elementPaddingHor = elementPadding.right + elementPadding.left;
const availableHeight = parentElementHeight - elementPaddingVer;
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
const geometry = {
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
};
return geometry;
};
}
export function fit(term) {
var geometry = proposeGeometry(term);
export function fit(term: Terminal): void {
const geometry = proposeGeometry(term);
if (geometry) {
// Force a full render
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
term.renderer.clear();
(<any>term).renderer.clear();
term.resize(geometry.cols, geometry.rows);
}
}
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.proposeGeometry = function() {
return proposeGeometry(this);
}
terminalConstructor.prototype.fit = function() {
return fit(this);
}
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).proposeGeometry = function (): IGeometry {
return proposeGeometry(this);
};
(<any>terminalConstructor.prototype).fit = function (): void {
fit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fit/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as fullscreen from './fullscreen'
import * as fullscreen from './fullscreen';
class MockTerminal {}
describe('fullscreen addon', () => {
describe('apply', () => {
it('should do register the `toggleFullscreen` method', () => {
fullscreen.apply(MockTerminal);
fullscreen.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.toggleFullScreen, 'function');
});
});
+13 -9
View File
@@ -3,15 +3,19 @@
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
* Toggle the given terminal's fullscreen mode.
* @param {Terminal} term - The terminal to toggle full screen mode
* @param {boolean} fullscreen - Toggle fullscreen on (true) or off (false)
* @param term The terminal to toggle full screen mode
* @param fullscreen Toggle fullscreen on (true) or off (false)
*/
export function toggleFullScreen(term, fullscreen) {
var fn;
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
let fn: string;
if (typeof fullscreen == 'undefined') {
if (typeof fullscreen === 'undefined') {
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
} else if (!fullscreen) {
fn = 'remove';
@@ -20,10 +24,10 @@ export function toggleFullScreen(term, fullscreen) {
}
term.element.classList[fn]('fullscreen');
};
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.toggleFullScreen = function (fullscreen) {
return toggleFullScreen(this, fullscreen);
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).toggleFullScreen = function (fullscreen: boolean): void {
toggleFullScreen(this, fullscreen);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/fullscreen/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
// TODO: Reuse ITerminal from core
buffer: any;
selectionManager: any;
}
export interface ISearchHelper {
findNext(term: string): boolean;
findPrevious(term: string): boolean;
}
+20 -6
View File
@@ -3,8 +3,7 @@
* @license MIT
*/
// import { ITerminal } from '../../Interfaces';
// import { translateBufferLineToString } from '../../utils/BufferLine';
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
interface ISearchResult {
term: string;
@@ -15,8 +14,8 @@ interface ISearchResult {
/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper {
constructor(private _terminal: any) {
export class SearchHelper implements ISearchHelper {
constructor(private _terminal: ISearchAddonTerminal) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
@@ -114,8 +113,23 @@ export class SearchHelper {
private _findInLine(term: string, y: number): ISearchResult {
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
const lowerTerm = term.toLowerCase();
const searchIndex = lowerStringLine.indexOf(lowerTerm);
let searchIndex = lowerStringLine.indexOf(lowerTerm);
if (searchIndex >= 0) {
const line = this._terminal.buffer.lines.get(y);
for (let i = 0; i < searchIndex; i++) {
const charData = line[i];
// Adjust the searchIndex to normalize emoji into single chars
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
searchIndex -= char.length - 1;
}
// Adjust the searchIndex for empty characters following wide unicode
// chars (eg. CJK)
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
if (charWidth === 0) {
searchIndex++;
}
}
return {
term,
col: searchIndex,
@@ -134,7 +148,7 @@ export class SearchHelper {
return false;
}
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp, false);
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
return true;
}
}
+25 -20
View File
@@ -3,8 +3,11 @@
* @license MIT
*/
import { SearchHelper } from './SearchHelper';
/// <reference path="../../../typings/xterm.d.ts"/>
import { SearchHelper } from './SearchHelper';
import { Terminal } from 'xterm';
import { ISearchAddonTerminal } from './Interfaces';
/**
* Find the next instance of the term, then scroll to and select it. If it
@@ -12,12 +15,13 @@ import { SearchHelper } from './SearchHelper';
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findNext(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
export function findNext(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return (<SearchHelper>terminal.searchHelper).findNext(term);
};
return addonTerminal.__searchHelper.findNext(term);
}
/**
* Find the previous instance of the term, then scroll to and select it. If it
@@ -25,19 +29,20 @@ export function findNext(terminal: any, term: string): boolean {
* @param term Tne search term.
* @return Whether a result was found.
*/
export function findPrevious(terminal: any, term: string): boolean {
if (!terminal._searchHelper) {
terminal.searchHelper = new SearchHelper(terminal);
}
return (<SearchHelper>terminal.searchHelper).findPrevious(term);
};
export function apply(terminalConstructor) {
terminalConstructor.prototype.findNext = function(term) {
return findNext(this, term);
}
terminalConstructor.prototype.findPrevious = function(term) {
return findPrevious(this, term);
export function findPrevious(terminal: Terminal, term: string): boolean {
const addonTerminal = <ISearchAddonTerminal>terminal;
if (!addonTerminal.__searchHelper) {
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
}
return addonTerminal.__searchHelper.findPrevious(term);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
return findNext(this, term);
};
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
return findPrevious(this, term);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/search/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+19
View File
@@ -0,0 +1,19 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*
* Implements the attach method, that attaches the terminal to a WebSocket stream.
*/
import { Terminal } from 'xterm';
export interface ITerminadoAddonTerminal extends Terminal {
__socket?: WebSocket;
__attachSocketBuffer?: string;
__getMessage?(ev: MessageEvent): void;
__flushBuffer?(): void;
__pushToBuffer?(data: string): void;
__sendData?(data: string): void;
__setSize?(size: {rows: number, cols: number}): void;
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as terminado from './terminado'
import * as terminado from './terminado';
class MockTerminal {}
describe('terminado addon', () => {
describe('apply', () => {
it('should do register the `terminadoAttach` and `terminadoDetach` methods', () => {
terminado.apply(MockTerminal);
terminado.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.terminadoAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.terminadoDetach, 'function');
});
+52 -51
View File
@@ -6,106 +6,107 @@
* WebSocket stream.
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { ITerminadoAddonTerminal } from './Interfaces';
/**
* Attaches the given terminal to the given socket.
*
* @param {Terminal} term - The terminal to be attached to the given socket.
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param term The terminal to be attached to the given socket.
* @param socket The socket to attach the current terminal.
* @param bidirectional Whether the terminal should send data to the socket as well.
* @param buffered Whether the rendering of incoming data should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
*/
export function terminadoAttach(term, socket, bidirectional, buffered) {
bidirectional = (typeof bidirectional == 'undefined') ? true : bidirectional;
term.socket = socket;
export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional;
addonTerminal.__socket = socket;
term._flushBuffer = function() {
term.write(term._attachSocketBuffer);
term._attachSocketBuffer = null;
addonTerminal.__flushBuffer = () => {
addonTerminal.write(addonTerminal.__attachSocketBuffer);
addonTerminal.__attachSocketBuffer = null;
};
term._pushToBuffer = function(data) {
if (term._attachSocketBuffer) {
term._attachSocketBuffer += data;
addonTerminal.__pushToBuffer = (data: string) => {
if (addonTerminal.__attachSocketBuffer) {
addonTerminal.__attachSocketBuffer += data;
} else {
term._attachSocketBuffer = data;
setTimeout(term._flushBuffer, 10);
addonTerminal.__attachSocketBuffer = data;
setTimeout(addonTerminal.__flushBuffer, 10);
}
};
term._getMessage = function(ev) {
var data = JSON.parse(ev.data)
if( data[0] == "stdout" ) {
addonTerminal.__getMessage = (ev: MessageEvent) => {
const data = JSON.parse(ev.data);
if (data[0] === 'stdout') {
if (buffered) {
term._pushToBuffer(data[1]);
addonTerminal.__pushToBuffer(data[1]);
} else {
term.write(data[1]);
addonTerminal.write(data[1]);
}
}
};
term._sendData = function(data) {
addonTerminal.__sendData = (data: string) => {
socket.send(JSON.stringify(['stdin', data]));
};
term._setSize = function(size) {
addonTerminal.__setSize = (size: {rows: number, cols: number}) => {
socket.send(JSON.stringify(['set_size', size.rows, size.cols]));
};
socket.addEventListener('message', term._getMessage);
socket.addEventListener('message', addonTerminal.__getMessage);
if (bidirectional) {
term.on('data', term._sendData);
addonTerminal.on('data', addonTerminal.__sendData);
}
term.on('resize', term._setSize);
addonTerminal.on('resize', addonTerminal.__setSize);
socket.addEventListener('close', term.terminadoDetach.bind(term, socket));
socket.addEventListener('error', term.terminadoDetach.bind(term, socket));
};
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
}
/**
* Detaches the given terminal from the given socket
*
* @param {Xterm} term - The terminal to be detached from the given socket.
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param term The terminal to be detached from the given socket.
* @param socket The socket from which to detach the current terminal.
*/
export function terminadoDetach(term, socket) {
term.off('data', term._sendData);
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
const addonTerminal = <ITerminadoAddonTerminal>term;
addonTerminal.off('data', addonTerminal.__sendData);
socket = (typeof socket == 'undefined') ? term.socket : socket;
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
if (socket) {
socket.removeEventListener('message', term._getMessage);
socket.removeEventListener('message', addonTerminal.__getMessage);
}
delete term.socket;
};
delete addonTerminal.__socket;
}
export function apply(terminalConstructor) {
export function apply(terminalConstructor: typeof Terminal): void {
/**
* Attaches the current terminal to the given socket
*
* @param {WebSocket} socket - The socket to attach the current terminal.
* @param {boolean} bidirectional - Whether the terminal should send data
* to the socket as well.
* @param {boolean} buffered - Whether the rendering of incoming data
* should happen instantly or at a maximum
* frequency of 1 rendering per 10ms.
* @param socket - The socket to attach the current terminal.
* @param bidirectional - Whether the terminal should send data to the socket as well.
* @param buffered - Whether the rendering of incoming data should happen instantly or at a
* maximum frequency of 1 rendering per 10ms.
*/
terminalConstructor.prototype.terminadoAttach = function(socket, bidirectional, buffered) {
(<any>terminalConstructor.prototype).terminadoAttach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void {
return terminadoAttach(this, socket, bidirectional, buffered);
};
/**
* Detaches the current terminal from the given socket.
*
* @param {WebSocket} socket - The socket from which to detach the current
* terminal.
* @param socket The socket from which to detach the current terminal.
*/
terminalConstructor.prototype.terminadoDetach = function(socket) {
(<any>terminalConstructor.prototype).terminadoDetach = function (socket: WebSocket): void {
return terminadoDetach(this, socket);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/terminado/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"name": "xterm.weblinks",
"main": "weblinks.js",
"private": true
}
+11
View File
@@ -0,0 +1,11 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"rootDir": ".",
"outDir": "../../../lib/addons/webLinks/",
"sourceMap": true,
"removeComments": true,
"declaration": true
}
}
+42
View File
@@ -0,0 +1,42 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert, expect } from 'chai';
import * as webLinks from './webLinks';
class MockTerminal {
public regex: RegExp;
public handler: (event: MouseEvent, uri: string) => void;
public options?: any;
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
this.regex = regex;
this.handler = handler;
this.options = options;
return 0;
}
}
describe('webLinks addon', () => {
describe('apply', () => {
it('should do register the `webLinksInit` method', () => {
webLinks.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
const row = ' http://foo.com/a~b#c~d?e~f ';
let match = row.match(term.regex);
let uri = match[term.options.matchIndex];
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
});
});
+48
View File
@@ -0,0 +1,48 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal, ILinkMatcherOptions } from 'xterm';
const protocolClause = '(https?:\\/\\/)';
const domainCharacterSet = '[\\da-z\\.-]+';
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
const domainBodyClause = '(' + domainCharacterSet + ')';
const tldClause = '([a-z\\.]{2,6})';
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
const localHostClause = '(localhost)';
const portClause = '(:\\d{1,5})';
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
const end = ')($|' + negatedPathCharacterSet + ')';
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
function handleLink(event: MouseEvent, uri: string): void {
window.open(uri, '_blank');
}
/**
* Initialize the web links addon, registering the link matcher.
* @param term The terminal to use web links within.
* @param handler A custom handler to use.
* @param options Custom options to use, matchIndex will always be ignored.
*/
export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
options.matchIndex = 1;
term.registerLinkMatcher(strictUrlRegex, handler, options);
}
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
webLinksInit(this, handler, options);
};
}
+10
View File
@@ -0,0 +1,10 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { Terminal } from 'xterm';
export interface IWinptyCompatAddonTerminal extends Terminal {
buffer: any;
}
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "xterm.winptyCompat",
"name": "xterm.winptycompat",
"main": "winptyCompat.js",
"private": true
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/winptyCompat/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as winptyCompat from './winptyCompat'
import * as winptyCompat from './winptyCompat';
class MockTerminal {}
describe('winptyCompat addon', () => {
describe('apply', () => {
it('should do register the `winptyCompatInit` method', () => {
winptyCompat.apply(MockTerminal);
winptyCompat.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
});
});
+34 -27
View File
@@ -3,36 +3,43 @@
* @license MIT
*/
export function winptyCompatInit(terminal): void {
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
import { IWinptyCompatAddonTerminal } from './Interfaces';
export function winptyCompatInit(terminal: Terminal): void {
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
// Don't do anything when the platform is not Windows
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
if (!isWindows) {
return;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
addonTerminal.on('linefeed', () => {
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
const lastChar = line[addonTerminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
// Winpty does not support wraparound mode which means that lines will never
// be marked as wrapped. This causes issues for things like copying a line
// retaining the wrapped new line characters or if consumers are listening
// in on the data stream.
//
// The workaround for this is to listen to every incoming line feed and mark
// the line as wrapped if the last character in the previous line is not a
// space. This is certainly not without its problems, but generally on
// Windows when text reaches the end of the terminal it's likely going to be
// wrapped.
terminal.on('linefeed', () => {
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
const lastChar = line[terminal.cols - 1];
if (lastChar[3] !== 32 /* ' ' */) {
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
(<any>nextLine).isWrapped = true;
}
});
});
}
export function apply(terminalConstructor) {
terminalConstructor.prototype.winptyCompatInit = function(): void {
export function apply(terminalConstructor: typeof Terminal): void {
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
winptyCompatInit(this);
};
}
+2 -1
View File
@@ -5,6 +5,7 @@
"rootDir": ".",
"outDir": "../../../lib/addons/zmodem/",
"sourceMap": true,
"removeComments": true
"removeComments": true,
"declaration": true
}
}
+2 -2
View File
@@ -5,14 +5,14 @@
import { assert, expect } from 'chai';
import * as zmodem from './zmodem'
import * as zmodem from './zmodem';
class MockTerminal {}
describe('zmodem addon', () => {
describe('apply', () => {
it('should do register the `zmodemAttach` method and `zmodemBrowser` attribute', () => {
zmodem.apply(MockTerminal);
zmodem.apply(<any>MockTerminal);
assert.equal(typeof (<any>MockTerminal).prototype.zmodemAttach, 'function');
assert.equal(typeof (<any>MockTerminal).prototype.zmodemBrowser, 'object');
});
+32 -29
View File
@@ -1,3 +1,12 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
/// <reference path="../../../typings/xterm.d.ts"/>
import { Terminal } from 'xterm';
/**
*
* Allow xterm.js to handle ZMODEM uploads and downloads.
@@ -27,47 +36,41 @@
* via `detach()` and a re-`attach()`.)
*/
let Zmodem;
let zmodem;
export function zmodemAttach(term, ws, opts) {
if (!opts) opts = {};
export interface IZmodemOptions {
noTerminalWriteOutsideSession?: boolean;
}
var senderFunc = function _ws_sender_func(octets) {
ws.send(new Uint8Array(octets));
};
function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
const term = this;
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
var zsentry;
let zsentry;
function _shouldWrite() {
function _shouldWrite(): boolean {
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
}
zsentry = new Zmodem.Sentry( {
to_terminal: function _to_terminal(octets) {
zsentry = new zmodem.Sentry({
to_terminal: (octets: ArrayLike<number>) => {
if (_shouldWrite()) {
term.write(
String.fromCharCode.apply(String, octets)
);
}
},
sender: senderFunc,
on_retract: () => (<any>term).emit('zmodemRetract'),
on_detect: (detection: any) => (<any>term).emit('zmodemDetect', detection)
});
on_retract: function _on_retract() {
term.emit('zmodemRetract');
},
function handleWSMessage(evt: MessageEvent): void {
on_detect: function _on_detect(detection) {
term.emit('zmodemDetect', detection);
},
} );
function handleWSMessage(evt) {
//In testing with xterm.js’s demo the first message was
//always text even if the rest were binary. While that
//may be specific to xterm.js’s demo, ultimately we
//should reject anything that isn’t binary.
// In testing with xterm.js’s demo the first message was
// always text even if the rest were binary. While that
// may be specific to xterm.js’s demo, ultimately we
// should reject anything that isn’t binary.
if (typeof evt.data === 'string') {
if (_shouldWrite()) {
term.write(evt.data);
@@ -82,9 +85,9 @@ export function zmodemAttach(term, ws, opts) {
ws.addEventListener('message', handleWSMessage);
}
export function apply(terminalConstructor) {
Zmodem = (typeof window == 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
export function apply(terminalConstructor: typeof Terminal): void {
zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests
terminalConstructor.prototype.zmodemAttach = zmodemAttach.bind(this, this);
terminalConstructor.prototype.zmodemBrowser = Zmodem.Browser
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
}
+268
View File
@@ -0,0 +1,268 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ICircularList, LineData } from '../Types';
import { C0 } from '../EscapeSequences';
const enum Direction {
UP = 'A',
DOWN = 'B',
RIGHT = 'C',
LEFT = 'D'
}
export class AltClickHandler {
private _startRow: number;
private _startCol: number;
private _endRow: number;
private _endCol: number;
private _lines: ICircularList<LineData>;
constructor(
private _mouseEvent: MouseEvent,
private _terminal: ITerminal
) {
this._lines = this._terminal.buffer.lines;
this._startCol = this._terminal.buffer.x;
this._startRow = this._terminal.buffer.y;
let coordinates = this._terminal.mouseHelper.getCoords(
this._mouseEvent,
this._terminal.element,
this._terminal.charMeasure,
this._terminal.options.lineHeight,
this._terminal.cols,
this._terminal.rows,
false
);
if (coordinates) {
[this._endCol, this._endRow] = coordinates.map((coordinate: number) => {
return coordinate - 1;
});
}
}
/**
* Writes the escape sequences of arrows to the terminal
*/
public move(): void {
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
this._terminal.send(this._arrowSequences());
}
}
/**
* Concatenates all the arrow sequences together.
* Resets the starting row to an unwrapped row, moves to the requested row,
* then moves to requested col.
*/
private _arrowSequences(): string {
// The alt buffer should try to navigate between rows
if (!this._terminal.buffer.hasScrollback) {
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
}
// Only move horizontally for the normal buffer
return this._moveHorizontallyOnly();
}
/**
* If the initial position of the cursor is on a row that is wrapped, move the
* cursor up to the first row that is not wrapped to have accurate vertical
* positioning.
*/
private _resetStartingRow(): string {
if (this._moveToRequestedRow().length === 0) {
return '';
}
return repeat(this._bufferLine(
this._startCol, this._startRow, this._startCol,
this._startRow - this._wrappedRowsForRow(this._startRow), false
).length, this._sequence(Direction.LEFT));
}
/**
* Using the reset starting and ending row, move to the requested row,
* ignoring wrapped rows
*/
private _moveToRequestedRow(): string {
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
}
/**
* Move to the requested col on the ending row
*/
private _moveToRequestedCol(): string {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
let endRow = this._endRow;
let direction = this._horizontalDirection();
return repeat(this._bufferLine(
this._startCol, startRow, this._endCol, endRow,
direction === Direction.RIGHT
).length, this._sequence(direction));
}
private _moveHorizontallyOnly(): string {
let direction = this._horizontalDirection();
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
}
/**
* Utility functions
*/
/**
* Calculates the number of wrapped rows between the unwrapped starting and
* ending rows. These rows need to ignored since the cursor skips over them.
*/
private _wrappedRowsCount(): number {
let wrappedRows = 0;
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
let direction = this._verticalDirection() === Direction.UP ? -1 : 1;
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
wrappedRows++;
}
}
return wrappedRows;
}
/**
* Calculates the number of wrapped rows that make up a given row.
* @param currentRow The row to determine how many wrapped rows make it up
*/
private _wrappedRowsForRow(currentRow: number): number {
let rowCount = 0;
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
rowCount++;
currentRow--;
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
}
return rowCount;
}
/**
* Direction determiners
*/
/**
* Determines if the right or left arrow is needed
*/
private _horizontalDirection(): Direction {
let startRow;
if (this._moveToRequestedRow().length > 0) {
startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
} else {
startRow = this._startRow;
}
if ((this._startCol < this._endCol &&
startRow <= this._endRow) || // down/right or same y/right
(this._startCol >= this._endCol &&
startRow < this._endRow)) { // down/left or same y/left
return Direction.RIGHT;
}
return Direction.LEFT;
}
/**
* Determines if the up or down arrow is needed
*/
private _verticalDirection(): Direction {
if (this._startRow > this._endRow) {
return Direction.UP;
}
return Direction.DOWN;
}
/**
* Constructs the string of chars in the buffer from a starting row and col
* to an ending row and col
* @param startCol The starting column position
* @param startRow The starting row position
* @param endCol The ending column position
* @param endRow The ending row position
* @param forward Direction to move
*/
private _bufferLine(
startCol: number,
startRow: number,
endCol: number,
endRow: number,
forward: boolean
): string {
let currentCol = startCol;
let currentRow = startRow;
let bufferStr = '';
while (currentCol !== endCol || currentRow !== endRow) {
currentCol += forward ? 1 : -1;
if (forward && currentCol > this._terminal.cols - 1) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
currentCol = 0;
startCol = 0;
currentRow++;
} else if (!forward && currentCol < 0) {
bufferStr += this._terminal.buffer.translateBufferLineToString(
currentRow, false, 0, startCol + 1
);
currentCol = this._terminal.cols - 1;
startCol = currentCol;
currentRow--;
}
}
return bufferStr + this._terminal.buffer.translateBufferLineToString(
currentRow, false, startCol, currentCol
);
}
/**
* Constructs the escape sequence for clicking an arrow
* @param direction The direction to move
*/
private _sequence(direction: Direction): string {
const mod = this._terminal.applicationCursor ? 'O' : '[';
return C0.ESC + mod + direction;
}
}
/**
* Returns a string repeated a given number of times
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
* @param count The number of times to repeat the string
* @param string The string that is to be repeated
*/
function repeat(count: number, str: string): string {
count = Math.floor(count);
let rpt = '';
for (let i = 0; i < count; i++) {
rpt += str;
}
return rpt;
}
+12 -7
View File
@@ -4,17 +4,22 @@
*/
import { assert } from 'chai';
import * as Terminal from '../Terminal';
import * as Clipboard from './Clipboard';
describe('evaluatePastedTextProcessing', () => {
it('should replace carriage return + line feed with line feed on windows', () => {
const pastedText = 'foo\r\nbar\r\n';
const processedText = Clipboard.prepareTextForTerminal(pastedText, false);
const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
it('should replace carriage return and/or line feed with carriage return', () => {
const pastedText = {
unix: 'foo\nbar\n',
windows: 'foo\r\nbar\r\n'
};
assert.equal(processedText, 'foo\r\nbar\r\n');
assert.equal(windowsProcessedText, 'foo\rbar\r');
const processedText = {
unix: Clipboard.prepareTextForTerminal(pastedText.unix),
windows: Clipboard.prepareTextForTerminal(pastedText.windows)
};
assert.equal(processedText.unix, 'foo\rbar\r');
assert.equal(processedText.windows, 'foo\rbar\r');
});
it('should bracket pasted text in bracketedPasteMode', () => {
const pastedText = 'foo bar';
+12 -9
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminal, ISelectionManager } from '../Interfaces';
import { ITerminal, ISelectionManager } from '../Types';
interface IWindow extends Window {
clipboardData?: {
@@ -18,11 +18,8 @@ declare var window: IWindow;
* Prepares text to be pasted into the terminal by normalizing the line endings
* @param text The pasted text that needs processing before inserting into the terminal
*/
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
if (isMSWindows) {
return text.replace(/\r?\n/g, '\r');
}
return text;
export function prepareTextForTerminal(text: string): string {
return text.replace(/\r?\n/g, '\r');
}
/**
@@ -62,7 +59,7 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
let text: string;
let dispatchPaste = function(text: string): void {
text = prepareTextForTerminal(text, term.browser.isMSWindows);
text = prepareTextForTerminal(text);
text = bracketTextForPaste(text, term.bracketedPasteMode);
term.handler(text);
term.textarea.value = '';
@@ -100,6 +97,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.focus();
// Reset the terminal textarea's styling
// Timeout needs to be long enough for click event to be handled.
setTimeout(() => {
textarea.style.position = null;
textarea.style.width = null;
@@ -107,7 +105,7 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
textarea.style.left = null;
textarea.style.top = null;
textarea.style.zIndex = null;
}, 4);
}, 200);
}
/**
@@ -115,10 +113,15 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
* @param ev The original right click event to be handled.
* @param textarea The terminal's textarea.
* @param selectionManager The terminal's selection manager.
* @param shouldSelectWord If true and there is no selection the current word will be selected
*/
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager): void {
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
moveTextAreaUnderMouseCursor(ev, textarea);
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
selectionManager.selectWordAtCursor(ev);
}
// Get textarea ready to copy from the context menu
textarea.value = selectionManager.selectionText;
textarea.select();
+31 -15
View File
@@ -3,8 +3,8 @@
* @license MIT
*/
import { IMouseZoneManager, IMouseZone } from './Interfaces';
import { ITerminal } from '../Interfaces';
import { ITerminal } from '../Types';
import { IMouseZoneManager, IMouseZone } from './Types';
const HOVER_DURATION = 500;
@@ -59,7 +59,9 @@ export class MouseZoneManager implements IMouseZoneManager {
// Iterate through zones and clear them out if they're within the range
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if (zone.y > start && zone.y <= end + 1) {
if ((zone.y1 > start && zone.y1 <= end + 1) ||
(zone.y2 > start && zone.y2 <= end + 1) ||
(zone.y1 < start && zone.y2 > end + 1)) {
if (this._currentZone && this._currentZone === zone) {
this._currentZone.leaveCallback();
this._currentZone = null;
@@ -151,10 +153,10 @@ export class MouseZoneManager implements IMouseZoneManager {
// components from handling the mouse event.
const zone = this._findZoneEventAt(e);
if (zone) {
// TODO: When link modifier support is added, the event should only be
// cancelled when the modifier is held (see #1021)
e.preventDefault();
e.stopImmediatePropagation();
if (zone.willLinkActivate(e)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
}
@@ -169,16 +171,28 @@ export class MouseZoneManager implements IMouseZoneManager {
}
private _findZoneEventAt(e: MouseEvent): IMouseZone {
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.element, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
if (!coords) {
return null;
}
const x = coords[0];
const y = coords[1];
for (let i = 0; i < this._zones.length; i++) {
const zone = this._zones[i];
if (zone.y === coords[1] && zone.x1 <= coords[0] && zone.x2 > coords[0]) {
return zone;
if (zone.y1 === zone.y2) {
// Single line link
if (y === zone.y1 && x >= zone.x1 && x < zone.x2) {
return zone;
}
} else {
// Multi-line link
if ((y === zone.y1 && x >= zone.x1) ||
(y === zone.y2 && x < zone.x2) ||
(y > zone.y1 && y < zone.y2)) {
return zone;
}
}
};
}
return null;
}
}
@@ -186,12 +200,14 @@ export class MouseZoneManager implements IMouseZoneManager {
export class MouseZone implements IMouseZone {
constructor(
public x1: number,
public y1: number,
public x2: number,
public y: number,
public y2: number,
public clickCallback: (e: MouseEvent) => any,
public hoverCallback?: (e: MouseEvent) => any,
public tooltipCallback?: (e: MouseEvent) => any,
public leaveCallback?: () => void
public hoverCallback: (e: MouseEvent) => any,
public tooltipCallback: (e: MouseEvent) => any,
public leaveCallback: () => void,
public willLinkActivate: (e: MouseEvent) => boolean
) {
}
}
@@ -11,9 +11,11 @@ export interface IMouseZoneManager {
export interface IMouseZone {
x1: number;
x2: number;
y: number;
y1: number;
y2: number;
clickCallback: (e: MouseEvent) => any;
hoverCallback?: (e: MouseEvent) => any;
tooltipCallback?: (e: MouseEvent) => any;
leaveCallback?: () => any;
hoverCallback: (e: MouseEvent) => any | undefined;
tooltipCallback: (e: MouseEvent) => any | undefined;
leaveCallback: () => any | undefined;
willLinkActivate: (e: MouseEvent) => boolean;
}
+71 -86
View File
@@ -3,14 +3,12 @@
* @license MIT
*/
import { IRenderLayer, IColorSet, IRenderDimensions } from './Interfaces';
import { ITerminal, ITerminalOptions } from '../Interfaces';
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
import { CharData } from '../Types';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
export const INVERTED_DEFAULT_COLOR = -1;
const DIM_OPACITY = 0.5;
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal } from '../Types';
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -22,25 +20,28 @@ export abstract class BaseRenderLayer implements IRenderLayer {
private _scaledCharLeft: number = 0;
private _scaledCharTop: number = 0;
private _charAtlas: HTMLCanvasElement | ImageBitmap;
protected _charAtlas: BaseCharAtlas;
constructor(
container: HTMLElement,
private _container: HTMLElement,
id: string,
zIndex: number,
private _alpha: boolean,
protected _colors: IColorSet
) {
this._canvas = document.createElement('canvas');
this._canvas.id = `xterm-${id}-layer`;
this._canvas.classList.add(`xterm-${id}-layer`);
this._canvas.style.zIndex = zIndex.toString();
this._ctx = this._canvas.getContext('2d', {alpha: _alpha});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
this._initCanvas();
this._container.appendChild(this._canvas);
}
private _initCanvas(): void {
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
// Draw the background if this is an opaque layer
if (!_alpha) {
if (!this._alpha) {
this.clearAll();
}
container.appendChild(this._canvas);
}
public onOptionsChanged(terminal: ITerminal): void {}
@@ -54,6 +55,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._refreshCharAtlas(terminal, colorSet);
}
protected setTransparency(terminal: ITerminal, alpha: boolean): void {
// Do nothing when alpha doesn't change
if (alpha === this._alpha) {
return;
}
// Create new canvas and replace old one
const oldCanvas = this._canvas;
this._alpha = alpha;
// Cloning preserves properties
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
this._initCanvas();
this._container.replaceChild(this._canvas, oldCanvas);
// Regenerate char atlas and force a full redraw
this._refreshCharAtlas(terminal, this._colors);
this.onGridChanged(terminal, 0, terminal.rows - 1);
}
/**
* Refreshes the char atlas, aquiring a new one if necessary.
* @param terminal The terminal.
@@ -63,16 +83,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
return;
}
this._charAtlas = null;
const result = acquireCharAtlas(terminal, this._colors, this._scaledCharWidth, this._scaledCharHeight);
if (result instanceof HTMLCanvasElement) {
this._charAtlas = result;
} else {
result.then(bitmap => this._charAtlas = bitmap);
}
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
this._charAtlas.warmUp();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
this._scaledCellWidth = dim.scaledCellWidth;
this._scaledCellHeight = dim.scaledCellHeight;
this._scaledCharWidth = dim.scaledCharWidth;
@@ -89,9 +104,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this.clearAll();
}
if (charSizeChanged) {
this._refreshCharAtlas(terminal, this._colors);
}
this._refreshCharAtlas(terminal, this._colors);
}
public abstract reset(terminal: ITerminal): void;
@@ -161,7 +174,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
if (this._alpha) {
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
}
}
@@ -181,7 +194,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
width * this._scaledCellWidth,
height * this._scaledCellHeight);
} else {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
this._ctx.fillRect(
x * this._scaledCellWidth,
y * this._scaledCellHeight,
@@ -201,7 +214,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param color The color of the character.
*/
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
this._ctx.font = this._getFont(terminal, false, false);
this._ctx.textBaseline = 'top';
this._clipRow(terminal, y);
this._ctx.fillText(
@@ -224,56 +237,19 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* This is used to validate whether a cached image can be used.
* @param bold Whether the text is bold.
*/
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void {
let colorIndex = 0;
if (fg < 256) {
colorIndex = fg + 2;
} else {
// If default color and bold
if (bold && terminal.options.enableBold) {
colorIndex = 1;
}
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
fg += drawInBrightColor ? 8 : 0;
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
this._ctx,
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop
);
if (!atlasDidDraw) {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
}
const isAscii = code < 256;
// A color is basic if it is one of the standard normal or bold weight
// colors of the characters held in the char atlas. Note that this excludes
// the normal weight _light_ color characters.
const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold);
const isDefaultColor = fg >= 256;
const isDefaultBackground = bg >= 256;
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
// ImageBitmap's draw about twice as fast as from a canvas
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
// Apply alpha to dim the character
if (dim) {
this._ctx.globalAlpha = DIM_OPACITY;
}
// Draw the non-bold version of the same color if bold is not enabled
if (bold && !terminal.options.enableBold) {
// Ignore default color as it's not touched above
if (colorIndex > 1) {
colorIndex -= 8;
}
}
this._ctx.drawImage(this._charAtlas,
code * charAtlasCellWidth,
colorIndex * charAtlasCellHeight,
charAtlasCellWidth,
this._scaledCharHeight,
x * this._scaledCellWidth + this._scaledCharLeft,
y * this._scaledCellHeight + this._scaledCharTop,
charAtlasCellWidth,
this._scaledCharHeight);
} else {
this._drawUncachedChar(terminal, char, width, fg, x, y, bold, dim);
}
// This draws the atlas (for debugging purposes)
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
// this._ctx.drawImage(this._charAtlas, 0, 0);
}
/**
@@ -287,21 +263,18 @@ export abstract class BaseRenderLayer implements IRenderLayer {
* @param x The column to draw at.
* @param y The row to draw at.
*/
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
this._ctx.save();
this._ctx.font = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
if (bold && terminal.options.enableBold) {
this._ctx.font = `bold ${this._ctx.font}`;
}
this._ctx.font = this._getFont(terminal, bold, italic);
this._ctx.textBaseline = 'top';
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground;
this._ctx.fillStyle = this._colors.foreground.css;
}
this._clipRow(terminal, y);
@@ -332,5 +305,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
this._scaledCellHeight);
this._ctx.clip();
}
/**
* Gets the current font.
* @param terminal The terminal.
* @param isBold If we should use the bold fontWeight.
*/
protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string {
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
const fontStyle = isItalic ? 'italic' : '';
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
}
}
-222
View File
@@ -1,222 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { IColorSet } from '../renderer/Interfaces';
import { isFirefox } from '../utils/Browser';
export const CHAR_ATLAS_CELL_SPACING = 1;
interface ICharAtlasConfig {
fontSize: number;
fontFamily: string;
scaledCharWidth: number;
scaledCharHeight: number;
colors: IColorSet;
}
interface ICharAtlasCacheEntry {
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
config: ICharAtlasConfig;
ownedBy: ITerminal[];
}
let charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.bitmap;
} else {
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.bitmap;
}
}
const newEntry: ICharAtlasCacheEntry = {
bitmap: generator.generate(scaledCharWidth, scaledCharHeight, terminal.options.fontSize, terminal.options.fontFamily, colors.background, colors.foreground, colors.ansi),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.bitmap;
}
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
ansi: colors.ansi.slice(0, 16)
};
return {
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
colors: clonedColors
};
}
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
return false;
}
}
return a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
let generator: CharAtlasGenerator;
/**
* Initializes the char atlas generator.
* @param document The document.
*/
export function initialize(document: Document): void {
if (!generator) {
generator = new CharAtlasGenerator(document);
}
}
class CharAtlasGenerator {
private _canvas: HTMLCanvasElement;
private _ctx: CanvasRenderingContext2D;
constructor(private _document: Document) {
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d', {alpha: false});
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
}
public generate(scaledCharWidth: number, scaledCharHeight: number, fontSize: number, fontFamily: string, background: string, foreground: string, ansiColors: string[]): HTMLCanvasElement | Promise<ImageBitmap> {
const cellWidth = scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
const cellHeight = scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
this._canvas.width = 255 * cellWidth;
this._canvas.height = (/*default+default bold*/2 + /*0-15*/16) * cellHeight;
this._ctx.fillStyle = background;
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
this._ctx.save();
this._ctx.fillStyle = foreground;
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
this._ctx.textBaseline = 'top';
// Default color
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, 0, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, 0);
this._ctx.restore();
}
// Default color bold
this._ctx.save();
this._ctx.font = `bold ${this._ctx.font}`;
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, cellHeight, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, cellHeight);
this._ctx.restore();
}
this._ctx.restore();
// Colors 0-15
this._ctx.font = `${fontSize * window.devicePixelRatio}px ${fontFamily}`;
for (let colorIndex = 0; colorIndex < 16; colorIndex++) {
// colors 8-15 are bold
if (colorIndex === 8) {
this._ctx.font = `bold ${this._ctx.font}`;
}
const y = (colorIndex + 2) * cellHeight;
// Draw ascii characters
for (let i = 0; i < 256; i++) {
this._ctx.save();
this._ctx.beginPath();
this._ctx.rect(i * cellWidth, y, cellWidth, cellHeight);
this._ctx.clip();
this._ctx.fillStyle = ansiColors[colorIndex];
this._ctx.fillText(String.fromCharCode(i), i * cellWidth, y);
this._ctx.restore();
}
}
this._ctx.restore();
// Support is patchy for createImageBitmap at the moment, pass a canvas back
// if support is lacking as drawImage works there too. Firefox is also
// included here as ImageBitmap appears both buggy and has horrible
// performance (tested on v55).
if (!('createImageBitmap' in window) || isFirefox) {
// Regenerate canvas and context as they are now owned by the char atlas
const result = this._canvas;
this._canvas = this._document.createElement('canvas');
this._ctx = this._canvas.getContext('2d');
this._ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
return result;
}
const charAtlasImageData = this._ctx.getImageData(0, 0, this._canvas.width, this._canvas.height);
// Remove the background color from the image so characters may overlap
const r = parseInt(background.substr(1, 2), 16);
const g = parseInt(background.substr(3, 2), 16);
const b = parseInt(background.substr(5, 2), 16);
this._clearColor(charAtlasImageData, r, g, b);
const promise = window.createImageBitmap(charAtlasImageData);
// Clear the rect while the promise is in progress
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
return promise;
}
private _clearColor(imageData: ImageData, r: number, g: number, b: number): void {
for (let offset = 0; offset < imageData.data.length; offset += 4) {
if (imageData.data[offset] === r &&
imageData.data[offset + 1] === g &&
imageData.data[offset + 2] === b) {
imageData.data[offset + 3] = 0;
}
}
}
}
+268 -250
View File
@@ -3,268 +3,286 @@
* @license MIT
*/
import jsdom = require('jsdom');
import { assert } from 'chai';
import { ColorManager } from './ColorManager';
describe('ColorManager', () => {
let cm: ColorManager;
let dom: jsdom.JSDOM;
let document: Document;
let window: Window;
beforeEach(() => {
cm = new ColorManager();
dom = new jsdom.JSDOM('');
window = dom.window;
document = window.document;
(<any>window).HTMLCanvasElement.prototype.getContext = () => ({
createLinearGradient(): any {
return null;
},
fillRect(): void { },
getImageData(): any {
return {data: [0, 0, 0, 0xFF]};
}
});
cm = new ColorManager(document, false);
});
describe('constructor', () => {
it('should fill all colors with values', () => {
for (let key in cm.colors) {
if (typeof key === 'string') {
for (let key of Object.keys(cm.colors)) {
if (key !== 'ansi') {
// A #rrggbb or rgba(...)
assert.ok(cm.colors[key].length >= 7);
assert.ok(cm.colors[key].css.length >= 7);
}
}
assert.equal(cm.colors.ansi.length, 256);
});
it('should fill 240 colors with expected values', () => {
assert.equal(cm.colors.ansi[16], '#000000');
assert.equal(cm.colors.ansi[17], '#00005f');
assert.equal(cm.colors.ansi[18], '#000087');
assert.equal(cm.colors.ansi[19], '#0000af');
assert.equal(cm.colors.ansi[20], '#0000d7');
assert.equal(cm.colors.ansi[21], '#0000ff');
assert.equal(cm.colors.ansi[22], '#005f00');
assert.equal(cm.colors.ansi[23], '#005f5f');
assert.equal(cm.colors.ansi[24], '#005f87');
assert.equal(cm.colors.ansi[25], '#005faf');
assert.equal(cm.colors.ansi[26], '#005fd7');
assert.equal(cm.colors.ansi[27], '#005fff');
assert.equal(cm.colors.ansi[28], '#008700');
assert.equal(cm.colors.ansi[29], '#00875f');
assert.equal(cm.colors.ansi[30], '#008787');
assert.equal(cm.colors.ansi[31], '#0087af');
assert.equal(cm.colors.ansi[32], '#0087d7');
assert.equal(cm.colors.ansi[33], '#0087ff');
assert.equal(cm.colors.ansi[34], '#00af00');
assert.equal(cm.colors.ansi[35], '#00af5f');
assert.equal(cm.colors.ansi[36], '#00af87');
assert.equal(cm.colors.ansi[37], '#00afaf');
assert.equal(cm.colors.ansi[38], '#00afd7');
assert.equal(cm.colors.ansi[39], '#00afff');
assert.equal(cm.colors.ansi[40], '#00d700');
assert.equal(cm.colors.ansi[41], '#00d75f');
assert.equal(cm.colors.ansi[42], '#00d787');
assert.equal(cm.colors.ansi[43], '#00d7af');
assert.equal(cm.colors.ansi[44], '#00d7d7');
assert.equal(cm.colors.ansi[45], '#00d7ff');
assert.equal(cm.colors.ansi[46], '#00ff00');
assert.equal(cm.colors.ansi[47], '#00ff5f');
assert.equal(cm.colors.ansi[48], '#00ff87');
assert.equal(cm.colors.ansi[49], '#00ffaf');
assert.equal(cm.colors.ansi[50], '#00ffd7');
assert.equal(cm.colors.ansi[51], '#00ffff');
assert.equal(cm.colors.ansi[52], '#5f0000');
assert.equal(cm.colors.ansi[53], '#5f005f');
assert.equal(cm.colors.ansi[54], '#5f0087');
assert.equal(cm.colors.ansi[55], '#5f00af');
assert.equal(cm.colors.ansi[56], '#5f00d7');
assert.equal(cm.colors.ansi[57], '#5f00ff');
assert.equal(cm.colors.ansi[58], '#5f5f00');
assert.equal(cm.colors.ansi[59], '#5f5f5f');
assert.equal(cm.colors.ansi[60], '#5f5f87');
assert.equal(cm.colors.ansi[61], '#5f5faf');
assert.equal(cm.colors.ansi[62], '#5f5fd7');
assert.equal(cm.colors.ansi[63], '#5f5fff');
assert.equal(cm.colors.ansi[64], '#5f8700');
assert.equal(cm.colors.ansi[65], '#5f875f');
assert.equal(cm.colors.ansi[66], '#5f8787');
assert.equal(cm.colors.ansi[67], '#5f87af');
assert.equal(cm.colors.ansi[68], '#5f87d7');
assert.equal(cm.colors.ansi[69], '#5f87ff');
assert.equal(cm.colors.ansi[70], '#5faf00');
assert.equal(cm.colors.ansi[71], '#5faf5f');
assert.equal(cm.colors.ansi[72], '#5faf87');
assert.equal(cm.colors.ansi[73], '#5fafaf');
assert.equal(cm.colors.ansi[74], '#5fafd7');
assert.equal(cm.colors.ansi[75], '#5fafff');
assert.equal(cm.colors.ansi[76], '#5fd700');
assert.equal(cm.colors.ansi[77], '#5fd75f');
assert.equal(cm.colors.ansi[78], '#5fd787');
assert.equal(cm.colors.ansi[79], '#5fd7af');
assert.equal(cm.colors.ansi[80], '#5fd7d7');
assert.equal(cm.colors.ansi[81], '#5fd7ff');
assert.equal(cm.colors.ansi[82], '#5fff00');
assert.equal(cm.colors.ansi[83], '#5fff5f');
assert.equal(cm.colors.ansi[84], '#5fff87');
assert.equal(cm.colors.ansi[85], '#5fffaf');
assert.equal(cm.colors.ansi[86], '#5fffd7');
assert.equal(cm.colors.ansi[87], '#5fffff');
assert.equal(cm.colors.ansi[88], '#870000');
assert.equal(cm.colors.ansi[89], '#87005f');
assert.equal(cm.colors.ansi[90], '#870087');
assert.equal(cm.colors.ansi[91], '#8700af');
assert.equal(cm.colors.ansi[92], '#8700d7');
assert.equal(cm.colors.ansi[93], '#8700ff');
assert.equal(cm.colors.ansi[94], '#875f00');
assert.equal(cm.colors.ansi[95], '#875f5f');
assert.equal(cm.colors.ansi[96], '#875f87');
assert.equal(cm.colors.ansi[97], '#875faf');
assert.equal(cm.colors.ansi[98], '#875fd7');
assert.equal(cm.colors.ansi[99], '#875fff');
assert.equal(cm.colors.ansi[100], '#878700');
assert.equal(cm.colors.ansi[101], '#87875f');
assert.equal(cm.colors.ansi[102], '#878787');
assert.equal(cm.colors.ansi[103], '#8787af');
assert.equal(cm.colors.ansi[104], '#8787d7');
assert.equal(cm.colors.ansi[105], '#8787ff');
assert.equal(cm.colors.ansi[106], '#87af00');
assert.equal(cm.colors.ansi[107], '#87af5f');
assert.equal(cm.colors.ansi[108], '#87af87');
assert.equal(cm.colors.ansi[109], '#87afaf');
assert.equal(cm.colors.ansi[110], '#87afd7');
assert.equal(cm.colors.ansi[111], '#87afff');
assert.equal(cm.colors.ansi[112], '#87d700');
assert.equal(cm.colors.ansi[113], '#87d75f');
assert.equal(cm.colors.ansi[114], '#87d787');
assert.equal(cm.colors.ansi[115], '#87d7af');
assert.equal(cm.colors.ansi[116], '#87d7d7');
assert.equal(cm.colors.ansi[117], '#87d7ff');
assert.equal(cm.colors.ansi[118], '#87ff00');
assert.equal(cm.colors.ansi[119], '#87ff5f');
assert.equal(cm.colors.ansi[120], '#87ff87');
assert.equal(cm.colors.ansi[121], '#87ffaf');
assert.equal(cm.colors.ansi[122], '#87ffd7');
assert.equal(cm.colors.ansi[123], '#87ffff');
assert.equal(cm.colors.ansi[124], '#af0000');
assert.equal(cm.colors.ansi[125], '#af005f');
assert.equal(cm.colors.ansi[126], '#af0087');
assert.equal(cm.colors.ansi[127], '#af00af');
assert.equal(cm.colors.ansi[128], '#af00d7');
assert.equal(cm.colors.ansi[129], '#af00ff');
assert.equal(cm.colors.ansi[130], '#af5f00');
assert.equal(cm.colors.ansi[131], '#af5f5f');
assert.equal(cm.colors.ansi[132], '#af5f87');
assert.equal(cm.colors.ansi[133], '#af5faf');
assert.equal(cm.colors.ansi[134], '#af5fd7');
assert.equal(cm.colors.ansi[135], '#af5fff');
assert.equal(cm.colors.ansi[136], '#af8700');
assert.equal(cm.colors.ansi[137], '#af875f');
assert.equal(cm.colors.ansi[138], '#af8787');
assert.equal(cm.colors.ansi[139], '#af87af');
assert.equal(cm.colors.ansi[140], '#af87d7');
assert.equal(cm.colors.ansi[141], '#af87ff');
assert.equal(cm.colors.ansi[142], '#afaf00');
assert.equal(cm.colors.ansi[143], '#afaf5f');
assert.equal(cm.colors.ansi[144], '#afaf87');
assert.equal(cm.colors.ansi[145], '#afafaf');
assert.equal(cm.colors.ansi[146], '#afafd7');
assert.equal(cm.colors.ansi[147], '#afafff');
assert.equal(cm.colors.ansi[148], '#afd700');
assert.equal(cm.colors.ansi[149], '#afd75f');
assert.equal(cm.colors.ansi[150], '#afd787');
assert.equal(cm.colors.ansi[151], '#afd7af');
assert.equal(cm.colors.ansi[152], '#afd7d7');
assert.equal(cm.colors.ansi[153], '#afd7ff');
assert.equal(cm.colors.ansi[154], '#afff00');
assert.equal(cm.colors.ansi[155], '#afff5f');
assert.equal(cm.colors.ansi[156], '#afff87');
assert.equal(cm.colors.ansi[157], '#afffaf');
assert.equal(cm.colors.ansi[158], '#afffd7');
assert.equal(cm.colors.ansi[159], '#afffff');
assert.equal(cm.colors.ansi[160], '#d70000');
assert.equal(cm.colors.ansi[161], '#d7005f');
assert.equal(cm.colors.ansi[162], '#d70087');
assert.equal(cm.colors.ansi[163], '#d700af');
assert.equal(cm.colors.ansi[164], '#d700d7');
assert.equal(cm.colors.ansi[165], '#d700ff');
assert.equal(cm.colors.ansi[166], '#d75f00');
assert.equal(cm.colors.ansi[167], '#d75f5f');
assert.equal(cm.colors.ansi[168], '#d75f87');
assert.equal(cm.colors.ansi[169], '#d75faf');
assert.equal(cm.colors.ansi[170], '#d75fd7');
assert.equal(cm.colors.ansi[171], '#d75fff');
assert.equal(cm.colors.ansi[172], '#d78700');
assert.equal(cm.colors.ansi[173], '#d7875f');
assert.equal(cm.colors.ansi[174], '#d78787');
assert.equal(cm.colors.ansi[175], '#d787af');
assert.equal(cm.colors.ansi[176], '#d787d7');
assert.equal(cm.colors.ansi[177], '#d787ff');
assert.equal(cm.colors.ansi[178], '#d7af00');
assert.equal(cm.colors.ansi[179], '#d7af5f');
assert.equal(cm.colors.ansi[180], '#d7af87');
assert.equal(cm.colors.ansi[181], '#d7afaf');
assert.equal(cm.colors.ansi[182], '#d7afd7');
assert.equal(cm.colors.ansi[183], '#d7afff');
assert.equal(cm.colors.ansi[184], '#d7d700');
assert.equal(cm.colors.ansi[185], '#d7d75f');
assert.equal(cm.colors.ansi[186], '#d7d787');
assert.equal(cm.colors.ansi[187], '#d7d7af');
assert.equal(cm.colors.ansi[188], '#d7d7d7');
assert.equal(cm.colors.ansi[189], '#d7d7ff');
assert.equal(cm.colors.ansi[190], '#d7ff00');
assert.equal(cm.colors.ansi[191], '#d7ff5f');
assert.equal(cm.colors.ansi[192], '#d7ff87');
assert.equal(cm.colors.ansi[193], '#d7ffaf');
assert.equal(cm.colors.ansi[194], '#d7ffd7');
assert.equal(cm.colors.ansi[195], '#d7ffff');
assert.equal(cm.colors.ansi[196], '#ff0000');
assert.equal(cm.colors.ansi[197], '#ff005f');
assert.equal(cm.colors.ansi[198], '#ff0087');
assert.equal(cm.colors.ansi[199], '#ff00af');
assert.equal(cm.colors.ansi[200], '#ff00d7');
assert.equal(cm.colors.ansi[201], '#ff00ff');
assert.equal(cm.colors.ansi[202], '#ff5f00');
assert.equal(cm.colors.ansi[203], '#ff5f5f');
assert.equal(cm.colors.ansi[204], '#ff5f87');
assert.equal(cm.colors.ansi[205], '#ff5faf');
assert.equal(cm.colors.ansi[206], '#ff5fd7');
assert.equal(cm.colors.ansi[207], '#ff5fff');
assert.equal(cm.colors.ansi[208], '#ff8700');
assert.equal(cm.colors.ansi[209], '#ff875f');
assert.equal(cm.colors.ansi[210], '#ff8787');
assert.equal(cm.colors.ansi[211], '#ff87af');
assert.equal(cm.colors.ansi[212], '#ff87d7');
assert.equal(cm.colors.ansi[213], '#ff87ff');
assert.equal(cm.colors.ansi[214], '#ffaf00');
assert.equal(cm.colors.ansi[215], '#ffaf5f');
assert.equal(cm.colors.ansi[216], '#ffaf87');
assert.equal(cm.colors.ansi[217], '#ffafaf');
assert.equal(cm.colors.ansi[218], '#ffafd7');
assert.equal(cm.colors.ansi[219], '#ffafff');
assert.equal(cm.colors.ansi[220], '#ffd700');
assert.equal(cm.colors.ansi[221], '#ffd75f');
assert.equal(cm.colors.ansi[222], '#ffd787');
assert.equal(cm.colors.ansi[223], '#ffd7af');
assert.equal(cm.colors.ansi[224], '#ffd7d7');
assert.equal(cm.colors.ansi[225], '#ffd7ff');
assert.equal(cm.colors.ansi[226], '#ffff00');
assert.equal(cm.colors.ansi[227], '#ffff5f');
assert.equal(cm.colors.ansi[228], '#ffff87');
assert.equal(cm.colors.ansi[229], '#ffffaf');
assert.equal(cm.colors.ansi[230], '#ffffd7');
assert.equal(cm.colors.ansi[231], '#ffffff');
assert.equal(cm.colors.ansi[232], '#080808');
assert.equal(cm.colors.ansi[233], '#121212');
assert.equal(cm.colors.ansi[234], '#1c1c1c');
assert.equal(cm.colors.ansi[235], '#262626');
assert.equal(cm.colors.ansi[236], '#303030');
assert.equal(cm.colors.ansi[237], '#3a3a3a');
assert.equal(cm.colors.ansi[238], '#444444');
assert.equal(cm.colors.ansi[239], '#4e4e4e');
assert.equal(cm.colors.ansi[240], '#585858');
assert.equal(cm.colors.ansi[241], '#626262');
assert.equal(cm.colors.ansi[242], '#6c6c6c');
assert.equal(cm.colors.ansi[243], '#767676');
assert.equal(cm.colors.ansi[244], '#808080');
assert.equal(cm.colors.ansi[245], '#8a8a8a');
assert.equal(cm.colors.ansi[246], '#949494');
assert.equal(cm.colors.ansi[247], '#9e9e9e');
assert.equal(cm.colors.ansi[248], '#a8a8a8');
assert.equal(cm.colors.ansi[249], '#b2b2b2');
assert.equal(cm.colors.ansi[250], '#bcbcbc');
assert.equal(cm.colors.ansi[251], '#c6c6c6');
assert.equal(cm.colors.ansi[252], '#d0d0d0');
assert.equal(cm.colors.ansi[253], '#dadada');
assert.equal(cm.colors.ansi[254], '#e4e4e4');
assert.equal(cm.colors.ansi[255], '#eeeeee');
assert.equal(cm.colors.ansi[16].css, '#000000');
assert.equal(cm.colors.ansi[17].css, '#00005f');
assert.equal(cm.colors.ansi[18].css, '#000087');
assert.equal(cm.colors.ansi[19].css, '#0000af');
assert.equal(cm.colors.ansi[20].css, '#0000d7');
assert.equal(cm.colors.ansi[21].css, '#0000ff');
assert.equal(cm.colors.ansi[22].css, '#005f00');
assert.equal(cm.colors.ansi[23].css, '#005f5f');
assert.equal(cm.colors.ansi[24].css, '#005f87');
assert.equal(cm.colors.ansi[25].css, '#005faf');
assert.equal(cm.colors.ansi[26].css, '#005fd7');
assert.equal(cm.colors.ansi[27].css, '#005fff');
assert.equal(cm.colors.ansi[28].css, '#008700');
assert.equal(cm.colors.ansi[29].css, '#00875f');
assert.equal(cm.colors.ansi[30].css, '#008787');
assert.equal(cm.colors.ansi[31].css, '#0087af');
assert.equal(cm.colors.ansi[32].css, '#0087d7');
assert.equal(cm.colors.ansi[33].css, '#0087ff');
assert.equal(cm.colors.ansi[34].css, '#00af00');
assert.equal(cm.colors.ansi[35].css, '#00af5f');
assert.equal(cm.colors.ansi[36].css, '#00af87');
assert.equal(cm.colors.ansi[37].css, '#00afaf');
assert.equal(cm.colors.ansi[38].css, '#00afd7');
assert.equal(cm.colors.ansi[39].css, '#00afff');
assert.equal(cm.colors.ansi[40].css, '#00d700');
assert.equal(cm.colors.ansi[41].css, '#00d75f');
assert.equal(cm.colors.ansi[42].css, '#00d787');
assert.equal(cm.colors.ansi[43].css, '#00d7af');
assert.equal(cm.colors.ansi[44].css, '#00d7d7');
assert.equal(cm.colors.ansi[45].css, '#00d7ff');
assert.equal(cm.colors.ansi[46].css, '#00ff00');
assert.equal(cm.colors.ansi[47].css, '#00ff5f');
assert.equal(cm.colors.ansi[48].css, '#00ff87');
assert.equal(cm.colors.ansi[49].css, '#00ffaf');
assert.equal(cm.colors.ansi[50].css, '#00ffd7');
assert.equal(cm.colors.ansi[51].css, '#00ffff');
assert.equal(cm.colors.ansi[52].css, '#5f0000');
assert.equal(cm.colors.ansi[53].css, '#5f005f');
assert.equal(cm.colors.ansi[54].css, '#5f0087');
assert.equal(cm.colors.ansi[55].css, '#5f00af');
assert.equal(cm.colors.ansi[56].css, '#5f00d7');
assert.equal(cm.colors.ansi[57].css, '#5f00ff');
assert.equal(cm.colors.ansi[58].css, '#5f5f00');
assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
assert.equal(cm.colors.ansi[60].css, '#5f5f87');
assert.equal(cm.colors.ansi[61].css, '#5f5faf');
assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
assert.equal(cm.colors.ansi[63].css, '#5f5fff');
assert.equal(cm.colors.ansi[64].css, '#5f8700');
assert.equal(cm.colors.ansi[65].css, '#5f875f');
assert.equal(cm.colors.ansi[66].css, '#5f8787');
assert.equal(cm.colors.ansi[67].css, '#5f87af');
assert.equal(cm.colors.ansi[68].css, '#5f87d7');
assert.equal(cm.colors.ansi[69].css, '#5f87ff');
assert.equal(cm.colors.ansi[70].css, '#5faf00');
assert.equal(cm.colors.ansi[71].css, '#5faf5f');
assert.equal(cm.colors.ansi[72].css, '#5faf87');
assert.equal(cm.colors.ansi[73].css, '#5fafaf');
assert.equal(cm.colors.ansi[74].css, '#5fafd7');
assert.equal(cm.colors.ansi[75].css, '#5fafff');
assert.equal(cm.colors.ansi[76].css, '#5fd700');
assert.equal(cm.colors.ansi[77].css, '#5fd75f');
assert.equal(cm.colors.ansi[78].css, '#5fd787');
assert.equal(cm.colors.ansi[79].css, '#5fd7af');
assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
assert.equal(cm.colors.ansi[82].css, '#5fff00');
assert.equal(cm.colors.ansi[83].css, '#5fff5f');
assert.equal(cm.colors.ansi[84].css, '#5fff87');
assert.equal(cm.colors.ansi[85].css, '#5fffaf');
assert.equal(cm.colors.ansi[86].css, '#5fffd7');
assert.equal(cm.colors.ansi[87].css, '#5fffff');
assert.equal(cm.colors.ansi[88].css, '#870000');
assert.equal(cm.colors.ansi[89].css, '#87005f');
assert.equal(cm.colors.ansi[90].css, '#870087');
assert.equal(cm.colors.ansi[91].css, '#8700af');
assert.equal(cm.colors.ansi[92].css, '#8700d7');
assert.equal(cm.colors.ansi[93].css, '#8700ff');
assert.equal(cm.colors.ansi[94].css, '#875f00');
assert.equal(cm.colors.ansi[95].css, '#875f5f');
assert.equal(cm.colors.ansi[96].css, '#875f87');
assert.equal(cm.colors.ansi[97].css, '#875faf');
assert.equal(cm.colors.ansi[98].css, '#875fd7');
assert.equal(cm.colors.ansi[99].css, '#875fff');
assert.equal(cm.colors.ansi[100].css, '#878700');
assert.equal(cm.colors.ansi[101].css, '#87875f');
assert.equal(cm.colors.ansi[102].css, '#878787');
assert.equal(cm.colors.ansi[103].css, '#8787af');
assert.equal(cm.colors.ansi[104].css, '#8787d7');
assert.equal(cm.colors.ansi[105].css, '#8787ff');
assert.equal(cm.colors.ansi[106].css, '#87af00');
assert.equal(cm.colors.ansi[107].css, '#87af5f');
assert.equal(cm.colors.ansi[108].css, '#87af87');
assert.equal(cm.colors.ansi[109].css, '#87afaf');
assert.equal(cm.colors.ansi[110].css, '#87afd7');
assert.equal(cm.colors.ansi[111].css, '#87afff');
assert.equal(cm.colors.ansi[112].css, '#87d700');
assert.equal(cm.colors.ansi[113].css, '#87d75f');
assert.equal(cm.colors.ansi[114].css, '#87d787');
assert.equal(cm.colors.ansi[115].css, '#87d7af');
assert.equal(cm.colors.ansi[116].css, '#87d7d7');
assert.equal(cm.colors.ansi[117].css, '#87d7ff');
assert.equal(cm.colors.ansi[118].css, '#87ff00');
assert.equal(cm.colors.ansi[119].css, '#87ff5f');
assert.equal(cm.colors.ansi[120].css, '#87ff87');
assert.equal(cm.colors.ansi[121].css, '#87ffaf');
assert.equal(cm.colors.ansi[122].css, '#87ffd7');
assert.equal(cm.colors.ansi[123].css, '#87ffff');
assert.equal(cm.colors.ansi[124].css, '#af0000');
assert.equal(cm.colors.ansi[125].css, '#af005f');
assert.equal(cm.colors.ansi[126].css, '#af0087');
assert.equal(cm.colors.ansi[127].css, '#af00af');
assert.equal(cm.colors.ansi[128].css, '#af00d7');
assert.equal(cm.colors.ansi[129].css, '#af00ff');
assert.equal(cm.colors.ansi[130].css, '#af5f00');
assert.equal(cm.colors.ansi[131].css, '#af5f5f');
assert.equal(cm.colors.ansi[132].css, '#af5f87');
assert.equal(cm.colors.ansi[133].css, '#af5faf');
assert.equal(cm.colors.ansi[134].css, '#af5fd7');
assert.equal(cm.colors.ansi[135].css, '#af5fff');
assert.equal(cm.colors.ansi[136].css, '#af8700');
assert.equal(cm.colors.ansi[137].css, '#af875f');
assert.equal(cm.colors.ansi[138].css, '#af8787');
assert.equal(cm.colors.ansi[139].css, '#af87af');
assert.equal(cm.colors.ansi[140].css, '#af87d7');
assert.equal(cm.colors.ansi[141].css, '#af87ff');
assert.equal(cm.colors.ansi[142].css, '#afaf00');
assert.equal(cm.colors.ansi[143].css, '#afaf5f');
assert.equal(cm.colors.ansi[144].css, '#afaf87');
assert.equal(cm.colors.ansi[145].css, '#afafaf');
assert.equal(cm.colors.ansi[146].css, '#afafd7');
assert.equal(cm.colors.ansi[147].css, '#afafff');
assert.equal(cm.colors.ansi[148].css, '#afd700');
assert.equal(cm.colors.ansi[149].css, '#afd75f');
assert.equal(cm.colors.ansi[150].css, '#afd787');
assert.equal(cm.colors.ansi[151].css, '#afd7af');
assert.equal(cm.colors.ansi[152].css, '#afd7d7');
assert.equal(cm.colors.ansi[153].css, '#afd7ff');
assert.equal(cm.colors.ansi[154].css, '#afff00');
assert.equal(cm.colors.ansi[155].css, '#afff5f');
assert.equal(cm.colors.ansi[156].css, '#afff87');
assert.equal(cm.colors.ansi[157].css, '#afffaf');
assert.equal(cm.colors.ansi[158].css, '#afffd7');
assert.equal(cm.colors.ansi[159].css, '#afffff');
assert.equal(cm.colors.ansi[160].css, '#d70000');
assert.equal(cm.colors.ansi[161].css, '#d7005f');
assert.equal(cm.colors.ansi[162].css, '#d70087');
assert.equal(cm.colors.ansi[163].css, '#d700af');
assert.equal(cm.colors.ansi[164].css, '#d700d7');
assert.equal(cm.colors.ansi[165].css, '#d700ff');
assert.equal(cm.colors.ansi[166].css, '#d75f00');
assert.equal(cm.colors.ansi[167].css, '#d75f5f');
assert.equal(cm.colors.ansi[168].css, '#d75f87');
assert.equal(cm.colors.ansi[169].css, '#d75faf');
assert.equal(cm.colors.ansi[170].css, '#d75fd7');
assert.equal(cm.colors.ansi[171].css, '#d75fff');
assert.equal(cm.colors.ansi[172].css, '#d78700');
assert.equal(cm.colors.ansi[173].css, '#d7875f');
assert.equal(cm.colors.ansi[174].css, '#d78787');
assert.equal(cm.colors.ansi[175].css, '#d787af');
assert.equal(cm.colors.ansi[176].css, '#d787d7');
assert.equal(cm.colors.ansi[177].css, '#d787ff');
assert.equal(cm.colors.ansi[178].css, '#d7af00');
assert.equal(cm.colors.ansi[179].css, '#d7af5f');
assert.equal(cm.colors.ansi[180].css, '#d7af87');
assert.equal(cm.colors.ansi[181].css, '#d7afaf');
assert.equal(cm.colors.ansi[182].css, '#d7afd7');
assert.equal(cm.colors.ansi[183].css, '#d7afff');
assert.equal(cm.colors.ansi[184].css, '#d7d700');
assert.equal(cm.colors.ansi[185].css, '#d7d75f');
assert.equal(cm.colors.ansi[186].css, '#d7d787');
assert.equal(cm.colors.ansi[187].css, '#d7d7af');
assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
assert.equal(cm.colors.ansi[190].css, '#d7ff00');
assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
assert.equal(cm.colors.ansi[192].css, '#d7ff87');
assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
assert.equal(cm.colors.ansi[195].css, '#d7ffff');
assert.equal(cm.colors.ansi[196].css, '#ff0000');
assert.equal(cm.colors.ansi[197].css, '#ff005f');
assert.equal(cm.colors.ansi[198].css, '#ff0087');
assert.equal(cm.colors.ansi[199].css, '#ff00af');
assert.equal(cm.colors.ansi[200].css, '#ff00d7');
assert.equal(cm.colors.ansi[201].css, '#ff00ff');
assert.equal(cm.colors.ansi[202].css, '#ff5f00');
assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
assert.equal(cm.colors.ansi[204].css, '#ff5f87');
assert.equal(cm.colors.ansi[205].css, '#ff5faf');
assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
assert.equal(cm.colors.ansi[207].css, '#ff5fff');
assert.equal(cm.colors.ansi[208].css, '#ff8700');
assert.equal(cm.colors.ansi[209].css, '#ff875f');
assert.equal(cm.colors.ansi[210].css, '#ff8787');
assert.equal(cm.colors.ansi[211].css, '#ff87af');
assert.equal(cm.colors.ansi[212].css, '#ff87d7');
assert.equal(cm.colors.ansi[213].css, '#ff87ff');
assert.equal(cm.colors.ansi[214].css, '#ffaf00');
assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
assert.equal(cm.colors.ansi[216].css, '#ffaf87');
assert.equal(cm.colors.ansi[217].css, '#ffafaf');
assert.equal(cm.colors.ansi[218].css, '#ffafd7');
assert.equal(cm.colors.ansi[219].css, '#ffafff');
assert.equal(cm.colors.ansi[220].css, '#ffd700');
assert.equal(cm.colors.ansi[221].css, '#ffd75f');
assert.equal(cm.colors.ansi[222].css, '#ffd787');
assert.equal(cm.colors.ansi[223].css, '#ffd7af');
assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
assert.equal(cm.colors.ansi[226].css, '#ffff00');
assert.equal(cm.colors.ansi[227].css, '#ffff5f');
assert.equal(cm.colors.ansi[228].css, '#ffff87');
assert.equal(cm.colors.ansi[229].css, '#ffffaf');
assert.equal(cm.colors.ansi[230].css, '#ffffd7');
assert.equal(cm.colors.ansi[231].css, '#ffffff');
assert.equal(cm.colors.ansi[232].css, '#080808');
assert.equal(cm.colors.ansi[233].css, '#121212');
assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
assert.equal(cm.colors.ansi[235].css, '#262626');
assert.equal(cm.colors.ansi[236].css, '#303030');
assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
assert.equal(cm.colors.ansi[238].css, '#444444');
assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
assert.equal(cm.colors.ansi[240].css, '#585858');
assert.equal(cm.colors.ansi[241].css, '#626262');
assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
assert.equal(cm.colors.ansi[243].css, '#767676');
assert.equal(cm.colors.ansi[244].css, '#808080');
assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
assert.equal(cm.colors.ansi[246].css, '#949494');
assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
assert.equal(cm.colors.ansi[253].css, '#dadada');
assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
assert.equal(cm.colors.ansi[255].css, '#eeeeee');
});
});
@@ -276,20 +294,20 @@ describe('ColorManager', () => {
});
it('should set a partial set of colors, using the default if not present', () => {
assert.equal(cm.colors.background, '#000000');
assert.equal(cm.colors.foreground, '#ffffff');
assert.equal(cm.colors.background.css, '#000000');
assert.equal(cm.colors.foreground.css, '#ffffff');
cm.setTheme({
background: '#FF0000',
foreground: '#00FF00'
});
assert.equal(cm.colors.background, '#FF0000');
assert.equal(cm.colors.foreground, '#00FF00');
assert.equal(cm.colors.background.css, '#FF0000');
assert.equal(cm.colors.foreground.css, '#00FF00');
cm.setTheme({
background: '#0000FF'
});
assert.equal(cm.colors.background, '#0000FF');
assert.equal(cm.colors.background.css, '#0000FF');
// FG reverts back to default
assert.equal(cm.colors.foreground, '#ffffff');
assert.equal(cm.colors.foreground.css, '#ffffff');
});
});
});
+133 -72
View File
@@ -3,58 +3,75 @@
* @license MIT
*/
import { IColorSet, IColorManager } from './Interfaces';
import { ITheme } from '../Interfaces';
import { IColorManager } from './Types';
import { IColor, IColorSet } from '../shared/Types';
import { ITheme } from 'xterm';
const DEFAULT_FOREGROUND = '#ffffff';
const DEFAULT_BACKGROUND = '#000000';
const DEFAULT_CURSOR = '#ffffff';
const DEFAULT_CURSOR_ACCENT = '#000000';
const DEFAULT_SELECTION = 'rgba(255, 255, 255, 0.3)';
export const DEFAULT_ANSI_COLORS = [
// dark:
'#2e3436',
'#cc0000',
'#4e9a06',
'#c4a000',
'#3465a4',
'#75507b',
'#06989a',
'#d3d7cf',
// bright:
'#555753',
'#ef2929',
'#8ae234',
'#fce94f',
'#729fcf',
'#ad7fa8',
'#34e2e2',
'#eeeeec'
];
const DEFAULT_FOREGROUND = fromHex('#ffffff');
const DEFAULT_BACKGROUND = fromHex('#000000');
const DEFAULT_CURSOR = fromHex('#ffffff');
const DEFAULT_CURSOR_ACCENT = fromHex('#000000');
const DEFAULT_SELECTION = {
css: 'rgba(255, 255, 255, 0.3)',
rgba: 0xFFFFFF77
};
/**
* Fills an existing 16 length string with the remaining 240 ANSI colors.
* @param first16Colors The first 16 ANSI colors.
*/
function generate256Colors(first16Colors: string[]): string[] {
let colors = first16Colors.slice();
// An IIFE to generate DEFAULT_ANSI_COLORS. Do not mutate DEFAULT_ANSI_COLORS, instead make a copy
// and mutate that.
export const DEFAULT_ANSI_COLORS = (() => {
const colors = [
// dark:
fromHex('#2e3436'),
fromHex('#cc0000'),
fromHex('#4e9a06'),
fromHex('#c4a000'),
fromHex('#3465a4'),
fromHex('#75507b'),
fromHex('#06989a'),
fromHex('#d3d7cf'),
// bright:
fromHex('#555753'),
fromHex('#ef2929'),
fromHex('#8ae234'),
fromHex('#fce94f'),
fromHex('#729fcf'),
fromHex('#ad7fa8'),
fromHex('#34e2e2'),
fromHex('#eeeeec')
];
// Fill in the remaining 240 ANSI colors.
// Generate colors (16-231)
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
for (let i = 0; i < 216; i++) {
const r = toPaddedHex(v[(i / 36) % 6 | 0]);
const g = toPaddedHex(v[(i / 6) % 6 | 0]);
const b = toPaddedHex(v[i % 6]);
colors.push(`#${r}${g}${b}`);
const r = v[(i / 36) % 6 | 0];
const g = v[(i / 6) % 6 | 0];
const b = v[i % 6];
colors.push({
css: `#${toPaddedHex(r)}${toPaddedHex(g)}${toPaddedHex(b)}`,
// Use >>> 0 to force a conversion to an unsigned int
rgba: ((r << 24) | (g << 16) | (b << 8) | 0xFF) >>> 0
});
}
// Generate greys (232-255)
for (let i = 0; i < 24; i++) {
const c = toPaddedHex(8 + i * 10);
colors.push(`#${c}${c}${c}`);
const c = 8 + i * 10;
const ch = toPaddedHex(c);
colors.push({
css: `#${ch}${ch}${ch}`,
rgba: ((c << 24) | (c << 16) | (c << 8) | 0xFF) >>> 0
});
}
return colors;
})();
function fromHex(css: string): IColor {
return {
css,
rgba: parseInt(css.slice(1), 16) << 8 | 0xFF
};
}
function toPaddedHex(c: number): string {
@@ -67,15 +84,23 @@ function toPaddedHex(c: number): string {
*/
export class ColorManager implements IColorManager {
public colors: IColorSet;
private _ctx: CanvasRenderingContext2D;
private _litmusColor: CanvasGradient;
constructor() {
constructor(document: Document, public allowTransparency: boolean) {
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
this._ctx = canvas.getContext('2d');
this._ctx.globalCompositeOperation = 'copy';
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
this.colors = {
foreground: DEFAULT_FOREGROUND,
background: DEFAULT_BACKGROUND,
cursor: DEFAULT_CURSOR,
cursorAccent: DEFAULT_CURSOR_ACCENT,
selection: DEFAULT_SELECTION,
ansi: generate256Colors(DEFAULT_ANSI_COLORS)
ansi: DEFAULT_ANSI_COLORS.slice()
};
}
@@ -85,42 +110,78 @@ export class ColorManager implements IColorManager {
* colors will be used where colors are not defined.
*/
public setTheme(theme: ITheme): void {
this.colors.foreground = theme.foreground || DEFAULT_FOREGROUND;
this.colors.background = this._validateColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = theme.cursor || DEFAULT_CURSOR;
this.colors.cursorAccent = theme.cursorAccent || DEFAULT_CURSOR_ACCENT;
this.colors.selection = theme.selection || DEFAULT_SELECTION;
this.colors.ansi[0] = theme.black || DEFAULT_ANSI_COLORS[0];
this.colors.ansi[1] = theme.red || DEFAULT_ANSI_COLORS[1];
this.colors.ansi[2] = theme.green || DEFAULT_ANSI_COLORS[2];
this.colors.ansi[3] = theme.yellow || DEFAULT_ANSI_COLORS[3];
this.colors.ansi[4] = theme.blue || DEFAULT_ANSI_COLORS[4];
this.colors.ansi[5] = theme.magenta || DEFAULT_ANSI_COLORS[5];
this.colors.ansi[6] = theme.cyan || DEFAULT_ANSI_COLORS[6];
this.colors.ansi[7] = theme.white || DEFAULT_ANSI_COLORS[7];
this.colors.ansi[8] = theme.brightBlack || DEFAULT_ANSI_COLORS[8];
this.colors.ansi[9] = theme.brightRed || DEFAULT_ANSI_COLORS[9];
this.colors.ansi[10] = theme.brightGreen || DEFAULT_ANSI_COLORS[10];
this.colors.ansi[11] = theme.brightYellow || DEFAULT_ANSI_COLORS[11];
this.colors.ansi[12] = theme.brightBlue || DEFAULT_ANSI_COLORS[12];
this.colors.ansi[13] = theme.brightMagenta || DEFAULT_ANSI_COLORS[13];
this.colors.ansi[14] = theme.brightCyan || DEFAULT_ANSI_COLORS[14];
this.colors.ansi[15] = theme.brightWhite || DEFAULT_ANSI_COLORS[15];
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
this.colors.selection = this._parseColor(theme.selection, DEFAULT_SELECTION, true);
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
}
private _validateColor(color: string, fallback: string): string {
if (!color) {
private _parseColor(
css: string,
fallback: IColor,
allowTransparency: boolean = this.allowTransparency
): IColor {
if (!css) {
return fallback;
}
if (color.length === 7 && color.charAt(0) === '#') {
return color;
// If parsing the value results in failure, then it must be ignored, and the attribute must
// retain its previous value.
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
this._ctx.fillStyle = this._litmusColor;
this._ctx.fillStyle = css;
if (typeof this._ctx.fillStyle !== 'string') {
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
return fallback;
}
if (color.length === 4 && color.charAt(0) === '#') {
const r = color.charAt(1);
const g = color.charAt(2);
const b = color.charAt(3);
return `#${r}${r}${g}${g}${b}${b}`;
this._ctx.fillRect(0, 0, 1, 1);
const data = this._ctx.getImageData(0, 0, 1, 1).data;
if (!allowTransparency && data[3] !== 0xFF) {
// Ideally we'd just ignore the alpha channel, but...
//
// Browsers may not give back exactly the same RGB values we put in, because most/all
// convert the color to a pre-multiplied representation. getImageData converts that back to
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
// on their own.
//
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
// into 0x00000000.
//
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
// pixels that have just been set using putImageData() might be returned to an equivalent
// getImageData() as different values."
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
//
// So let's just use the fallback color in this case instead.
console.warn(
`Color: ${css} is using transparency, but allowTransparency is false. ` +
`Using fallback ${fallback.css}.`
);
return fallback;
}
return fallback;
return {
css,
rgba: (data[0] << 24 | data[1] << 16 | data[2] << 8 | data[3]) >>> 0
};
}
}
+22 -25
View File
@@ -3,15 +3,12 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ITerminalOptions } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
import { CharData } from '../Types';
import { CharData, ITerminal } from '../Types';
interface CursorState {
interface ICursorState {
x: number;
y: number;
isFocused: boolean;
@@ -25,10 +22,9 @@ interface CursorState {
const BLINK_INTERVAL = 600;
export class CursorRenderLayer extends BaseRenderLayer {
private _state: CursorState;
private _state: ICursorState;
private _cursorRenderers: {[key: string]: (terminal: ITerminal, x: number, y: number, charData: CharData) => void};
private _cursorBlinkStateManager: CursorBlinkStateManager;
private _isFocused: boolean;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'cursor', zIndex, true, colors);
@@ -37,7 +33,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
this._cursorRenderers = {
'bar': this._renderBarCursor.bind(this),
@@ -47,15 +43,15 @@ export class CursorRenderLayer extends BaseRenderLayer {
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
x: null,
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
}
@@ -108,9 +104,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Only render if the animation frame is not active
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
this._render(terminal, false);
} else {
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
}
}
@@ -138,7 +135,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
if (!terminal.isFocused) {
this._clearCursor();
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this._renderBlurCursor(terminal, terminal.buffer.x, viewportRelativeCursorY, charData);
this._ctx.restore();
this._state.x = terminal.buffer.x;
@@ -186,37 +183,37 @@ export class CursorRenderLayer extends BaseRenderLayer {
y: null,
isFocused: null,
style: null,
width: null,
width: null
};
}
}
private _renderBarCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillLeftLineAtCell(x, y);
this._ctx.restore();
}
private _renderBlockCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillCells(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.fillStyle = this._colors.cursorAccent;
this._ctx.fillStyle = this._colors.cursorAccent.css;
this.fillCharTrueColor(terminal, charData, x, y);
this._ctx.restore();
}
private _renderUnderlineCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.fillStyle = this._colors.cursor;
this._ctx.fillStyle = this._colors.cursor.css;
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
private _renderBlurCursor(terminal: ITerminal, x: number, y: number, charData: CharData): void {
this._ctx.save();
this._ctx.strokeStyle = this._colors.cursor;
this._ctx.strokeStyle = this._colors.cursor.css;
this.strokeRectAtCell(x, y, charData[CHAR_DATA_WIDTH_INDEX], 1);
this._ctx.restore();
}
@@ -238,7 +235,7 @@ class CursorBlinkStateManager {
constructor(
terminal: ITerminal,
private renderCallback: () => void
private _renderCallback: () => void
) {
this.isCursorVisible = true;
if (terminal.isFocused) {
@@ -273,7 +270,7 @@ class CursorBlinkStateManager {
this.isCursorVisible = true;
if (!this._animationFrame) {
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
}
@@ -304,7 +301,7 @@ class CursorBlinkStateManager {
// Hide the cursor
this.isCursorVisible = false;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
@@ -323,7 +320,7 @@ class CursorBlinkStateManager {
// Invert visibility and render
this.isCursorVisible = !this.isCursorVisible;
this._animationFrame = window.requestAnimationFrame(() => {
this.renderCallback();
this._renderCallback();
this._animationFrame = null;
});
}, BLINK_INTERVAL);
-99
View File
@@ -1,99 +0,0 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal, ITerminalOptions, ITheme, IEventEmitter } from '../Interfaces';
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number, didCharSizeChange: boolean): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
queueRefresh(start: number, end: number): void;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
export interface IColorManager {
colors: IColorSet;
}
export interface IColorSet {
foreground: string;
background: string;
cursor: string;
cursorAccent: string;
selection: string;
ansi: string[];
}
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
+28 -17
View File
@@ -3,25 +3,21 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal, ILinkifierAccessor } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { LinkHoverEvent, LinkHoverEventTypes } from '../Types';
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class LinkRenderLayer extends BaseRenderLayer {
private _state: LinkHoverEvent = null;
private _state: ILinkHoverEvent = null;
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
super(container, 'link', zIndex, true, colors);
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: LinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: LinkHoverEvent) => this._onLinkLeave(e));
terminal.linkifier.on(LinkHoverEventTypes.HOVER, (e: ILinkHoverEvent) => this._onLinkHover(e));
terminal.linkifier.on(LinkHoverEventTypes.LEAVE, (e: ILinkHoverEvent) => this._onLinkLeave(e));
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = null;
}
@@ -32,18 +28,33 @@ export class LinkRenderLayer extends BaseRenderLayer {
private _clearCurrentLink(): void {
if (this._state) {
this.clearCells(this._state.x, this._state.y, this._state.length, 1);
this.clearCells(this._state.x1, this._state.y1, this._state.cols - this._state.x1, 1);
const middleRowCount = this._state.y2 - this._state.y1 - 1;
if (middleRowCount > 0) {
this.clearCells(0, this._state.y1 + 1, this._state.cols, middleRowCount);
}
this.clearCells(0, this._state.y2, this._state.x2, 1);
this._state = null;
}
}
private _onLinkHover(e: LinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground;
this.fillBottomLineAtCells(e.x, e.y, e.length);
private _onLinkHover(e: ILinkHoverEvent): void {
this._ctx.fillStyle = this._colors.foreground.css;
if (e.y1 === e.y2) {
// Single line link
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
} else {
// Multi-line link
this.fillBottomLineAtCells(e.x1, e.y1, e.cols - e.x1);
for (let y = e.y1 + 1; y < e.y2; y++) {
this.fillBottomLineAtCells(0, y, e.cols);
}
this.fillBottomLineAtCells(0, e.y2, e.x2);
}
this._state = e;
}
private _onLinkLeave(e: LinkHoverEvent): void {
private _onLinkLeave(e: ILinkHoverEvent): void {
this._clearCurrentLink();
}
}
+74 -56
View File
@@ -3,39 +3,43 @@
* @license MIT
*/
import { ITerminal, ITheme } from '../Interfaces';
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
import { TextRenderLayer } from './TextRenderLayer';
import { SelectionRenderLayer } from './SelectionRenderLayer';
import { CursorRenderLayer } from './CursorRenderLayer';
import { ColorManager } from './ColorManager';
import { BaseRenderLayer } from './BaseRenderLayer';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Interfaces';
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
import { ITerminal } from '../Types';
import { LinkRenderLayer } from './LinkRenderLayer';
import { EventEmitter } from '../EventEmitter';
import { RenderDebouncer } from '../utils/RenderDebouncer';
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
import { ITheme } from 'xterm';
export class Renderer extends EventEmitter implements IRenderer {
/** A queue of the rows to be refreshed */
private _refreshRowsQueue: {start: number, end: number}[] = [];
private _refreshAnimationFrame = null;
private _renderDebouncer: RenderDebouncer;
private _renderLayers: IRenderLayer[];
private _devicePixelRatio: number;
private _screenDprMonitor: ScreenDprMonitor;
private _isPaused: boolean = false;
private _needsFullRefresh: boolean = false;
public colorManager: ColorManager;
public dimensions: IRenderDimensions;
constructor(private _terminal: ITerminal, theme: ITheme) {
super();
this.colorManager = new ColorManager();
const allowTransparency = this._terminal.options.allowTransparency;
this.colorManager = new ColorManager(document, allowTransparency);
if (theme) {
this.colorManager.setTheme(theme);
}
this._renderLayers = [
new TextRenderLayer(this._terminal.element, 0, this.colorManager.colors),
new SelectionRenderLayer(this._terminal.element, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.element, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.element, 3, this.colorManager.colors)
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
];
this.dimensions = {
scaledCharWidth: null,
@@ -53,6 +57,25 @@ export class Renderer extends EventEmitter implements IRenderer {
};
this._devicePixelRatio = window.devicePixelRatio;
this._updateDimensions();
this.onOptionsChanged();
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
this._screenDprMonitor = new ScreenDprMonitor();
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
// Detect whether IntersectionObserver is detected and enable renderer pause
// and resume based on terminal visibility if so
if ('IntersectionObserver' in window) {
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
observer.observe(this._terminal.element);
}
}
public onIntersectionChange(entry: IntersectionObserverEntry): void {
this._isPaused = entry.intersectionRatio === 0;
if (!this._isPaused && this._needsFullRefresh) {
this._terminal.refresh(0, this._terminal.rows - 1);
}
}
public onWindowResize(devicePixelRatio: number): void {
@@ -60,7 +83,7 @@ export class Renderer extends EventEmitter implements IRenderer {
// and the terminal needs to refreshed
if (this._devicePixelRatio !== devicePixelRatio) {
this._devicePixelRatio = devicePixelRatio;
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
}
@@ -73,20 +96,32 @@ export class Renderer extends EventEmitter implements IRenderer {
l.reset(this._terminal);
});
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
return this.colorManager.colors;
}
public onResize(cols: number, rows: number, didCharSizeChange: boolean): void {
public onResize(cols: number, rows: number): void {
// Update character and canvas dimensions
this._updateDimensions();
// Resize all render layers
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions, didCharSizeChange));
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
// Force a refresh
this._terminal.refresh(0, this._terminal.rows - 1);
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._terminal.refresh(0, this._terminal.rows - 1);
}
// Resize the screen
this._terminal.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
this._terminal.screenElement.style.height = `${this.dimensions.canvasHeight}px`;
this.emit('resize', {
width: this.dimensions.canvasWidth,
@@ -95,31 +130,40 @@ export class Renderer extends EventEmitter implements IRenderer {
}
public onCharSizeChanged(): void {
this.onResize(this._terminal.cols, this._terminal.rows, true);
this.onResize(this._terminal.cols, this._terminal.rows);
}
public onBlur(): void {
this._renderLayers.forEach(l => l.onBlur(this._terminal));
this._runOperation(l => l.onBlur(this._terminal));
}
public onFocus(): void {
this._renderLayers.forEach(l => l.onFocus(this._terminal));
this._runOperation(l => l.onFocus(this._terminal));
}
public onSelectionChanged(start: [number, number], end: [number, number]): void {
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end));
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
}
public onCursorMove(): void {
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
this._runOperation(l => l.onCursorMove(this._terminal));
}
public onOptionsChanged(): void {
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
this.colorManager.allowTransparency = this._terminal.options.allowTransparency;
this._runOperation(l => l.onOptionsChanged(this._terminal));
}
public clear(): void {
this._renderLayers.forEach(l => l.reset(this._terminal));
this._runOperation(l => l.reset(this._terminal));
}
private _runOperation(operation: (layer: IRenderLayer) => void): void {
if (this._isPaused) {
this._needsFullRefresh = true;
} else {
this._renderLayers.forEach(l => operation(l));
}
}
/**
@@ -128,43 +172,19 @@ export class Renderer extends EventEmitter implements IRenderer {
* @param {number} start The start row.
* @param {number} end The end row.
*/
public queueRefresh(start: number, end: number): void {
this._refreshRowsQueue.push({ start: start, end: end });
if (!this._refreshAnimationFrame) {
this._refreshAnimationFrame = window.requestAnimationFrame(this._refreshLoop.bind(this));
public refreshRows(start: number, end: number): void {
if (this._isPaused) {
this._needsFullRefresh = true;
return;
}
this._renderDebouncer.refresh(start, end);
}
/**
* Performs the refresh loop callback, calling refresh only if a refresh is
* necessary before queueing up the next one.
*/
private _refreshLoop(): void {
let start;
let end;
if (this._refreshRowsQueue.length > 4) {
// Just do a full refresh when 5+ refreshes are queued
start = 0;
end = this._terminal.rows - 1;
} else {
// Get start and end rows that need refreshing
start = this._refreshRowsQueue[0].start;
end = this._refreshRowsQueue[0].end;
for (let i = 1; i < this._refreshRowsQueue.length; i++) {
if (this._refreshRowsQueue[i].start < start) {
start = this._refreshRowsQueue[i].start;
}
if (this._refreshRowsQueue[i].end > end) {
end = this._refreshRowsQueue[i].end;
}
}
}
this._refreshRowsQueue = [];
this._refreshAnimationFrame = null;
// Render
start = Math.max(start, 0);
end = Math.min(end, this._terminal.rows - 1);
private _renderRows(start: number, end: number): void {
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
this._terminal.emit('refresh', {start, end});
}
@@ -227,7 +247,5 @@ export class Renderer extends EventEmitter implements IRenderer {
// differ.
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
}
}
+5 -8
View File
@@ -3,11 +3,8 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX } from '../Buffer';
import { GridCache } from './GridCache';
import { FLAGS } from './Types';
import { ITerminal } from '../Types';
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
export class SelectionRenderLayer extends BaseRenderLayer {
@@ -21,8 +18,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
};
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Resizing the canvas discards the contents of the canvas so clear state
this._state = {
start: null,
@@ -68,7 +65,7 @@ export class SelectionRenderLayer extends BaseRenderLayer {
// Draw first row
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
this._ctx.fillStyle = this._colors.selection;
this._ctx.fillStyle = this._colors.selection.css;
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
// Draw middle rows
+141 -114
View File
@@ -3,20 +3,19 @@
* @license MIT
*/
import { IColorSet, IRenderDimensions } from './Interfaces';
import { IBuffer, ICharMeasure, ITerminal } from '../Interfaces';
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
import { FLAGS } from './Types';
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
import { CharData, ITerminal } from '../Types';
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
import { GridCache } from './GridCache';
import { CharData } from '../Types';
import { BaseRenderLayer, INVERTED_DEFAULT_COLOR } from './BaseRenderLayer';
import { BaseRenderLayer } from './BaseRenderLayer';
/**
* This CharData looks like a null character, which will forc a clear and render
* when the character changes (a regular space ' ' character may not as it's
* drawn state is a cleared cell).
*/
const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
// const OVERLAP_OWNED_CHAR_DATA: CharData = [null, '', 0, -1];
export class TextRenderLayer extends BaseRenderLayer {
private _state: GridCache<CharData>;
@@ -24,16 +23,16 @@ export class TextRenderLayer extends BaseRenderLayer {
private _characterFont: string;
private _characterOverlapCache: { [key: string]: boolean } = {};
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
super(container, 'text', zIndex, false, colors);
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
super(container, 'text', zIndex, alpha, colors);
this._state = new GridCache<CharData>();
}
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
super.resize(terminal, dim, charSizeChanged);
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
super.resize(terminal, dim);
// Clear the character width cache if the font or width has changed
const terminalFont = `${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
const terminalFont = this._getFont(terminal, false, false);
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
this._characterWidth = dim.scaledCharWidth;
this._characterFont = terminalFont;
@@ -49,21 +48,24 @@ export class TextRenderLayer extends BaseRenderLayer {
this.clearAll();
}
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
for (let y = startRow; y <= endRow; y++) {
private _forEachCell(
terminal: ITerminal,
firstRow: number,
lastRow: number,
callback: (
code: number,
char: string,
width: number,
x: number,
y: number,
fg: number,
bg: number,
flags: number
) => void
): void {
for (let y = firstRow; y <= lastRow; y++) {
const row = y + terminal.buffer.ydisp;
const line = terminal.buffer.lines.get(row);
this.clearCells(0, y, terminal.cols, 1);
// for (let x = 0; x < terminal.cols; x++) {
// this._state.cache[x][y] = null;
// }
for (let x = 0; x < terminal.cols; x++) {
const charData = line[x];
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
@@ -74,51 +76,12 @@ export class TextRenderLayer extends BaseRenderLayer {
// The character to the left is a wide character, drawing is owned by
// the char at x-1
if (width === 0) {
// this._state.cache[x][y] = null;
continue;
}
// If the character is a space and the character to the left is an
// overlapping character, skip the character and allow the overlapping
// char to take full control over this character's cell.
if (code === 32 /*' '*/) {
if (x > 0) {
const previousChar: CharData = line[x - 1];
if (this._isOverlapping(previousChar)) {
continue;
}
}
}
// Skip rendering if the character is identical
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_CHAR_INDEX] === char && state[CHAR_DATA_ATTR_INDEX] === attr) {
// // Skip render, contents are identical
// this._state.cache[x][y] = charData;
// continue;
// }
// Clear the old character was not a space with the default background
// const wasInverted = !!(state && state[CHAR_DATA_ATTR_INDEX] && state[CHAR_DATA_ATTR_INDEX] >> 18 & FLAGS.INVERSE);
// if (state && !(state[CHAR_DATA_CODE_INDEX] === 32 /*' '*/ && (state[CHAR_DATA_ATTR_INDEX] & 0x1ff) >= 256 && !wasInverted)) {
// this._clearChar(x, y);
// }
// this._state.cache[x][y] = charData;
const flags = attr >> 18;
let bg = attr & 0x1ff;
// Skip rendering if the character is invisible
const isDefaultBackground = bg >= 256;
const isInvisible = flags & FLAGS.INVISIBLE;
const isInverted = flags & FLAGS.INVERSE;
if (!code || (code === 32 /*' '*/ && isDefaultBackground && !isInverted) || isInvisible) {
continue;
}
// If the character is an overlapping char and the character to the right is a
// space, take ownership of the cell to the right.
if (width !== 0 && this._isOverlapping(charData)) {
if (this._isOverlapping(charData)) {
// If the character is overlapping, we want to force a re-render on every
// frame. This is specifically to work around the case where two
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
@@ -136,10 +99,12 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
const flags = attr >> 18;
let bg = attr & 0x1ff;
let fg = (attr >> 9) & 0x1ff;
// If inverse flag is on, the foreground should become the background.
if (isInverted) {
if (flags & FLAGS.INVERSE) {
const temp = bg;
bg = fg;
fg = temp;
@@ -151,50 +116,112 @@ export class TextRenderLayer extends BaseRenderLayer {
}
}
// Clear the cell next to this character if it's wide
if (width === 2) {
// this.clearCells(x + 1, y, 1, 1);
}
// Draw background
if (bg < 256) {
this._ctx.save();
this._ctx.fillStyle = (bg === INVERTED_DEFAULT_COLOR ? this._colors.foreground : this._colors.ansi[bg]);
this.fillCells(x, y, width, 1);
this._ctx.restore();
}
this._ctx.save();
if (flags & FLAGS.BOLD) {
this._ctx.font = `bold ${this._ctx.font}`;
// Convert the FG color to the bold variant
if (fg < 8) {
fg += 8;
}
}
if (flags & FLAGS.UNDERLINE) {
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg];
} else {
this._ctx.fillStyle = this._colors.foreground;
}
this.fillBottomLineAtCells(x, y);
}
this.drawChar(terminal, char, code, width, x, y, fg, bg, !!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM));
this._ctx.restore();
callback(code, char, width, x, y, fg, bg, flags);
}
}
}
/**
* Whether a character is overlapping to the next cell.
*/
/**
* Draws the background for a specified range of columns. Tries to batch adjacent cells of the
* same color together to reduce draw calls.
*/
private _drawBackground(terminal: ITerminal, firstRow: number, lastRow: number): void {
const ctx = this._ctx;
const cols = terminal.cols;
let startX: number = 0;
let startY: number = 0;
let prevFillStyle: string | null = null;
ctx.save();
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
// libvte and xterm both draw the background (but not foreground) of invisible characters,
// so we should too.
let nextFillStyle = null; // null represents default background color
if (bg === INVERTED_DEFAULT_COLOR) {
nextFillStyle = this._colors.foreground.css;
} else if (bg < 256) {
nextFillStyle = this._colors.ansi[bg].css;
}
if (prevFillStyle === null) {
// This is either the first iteration, or the default background was set. Either way, we
// don't need to draw anything.
startX = x;
startY = y;
} if (y !== startY) {
// our row changed, draw the previous row
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, cols - startX, 1);
startX = x;
startY = y;
} else if (prevFillStyle !== nextFillStyle) {
// our color changed, draw the previous characters in this row
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, x - startX, 1);
startX = x;
startY = y;
}
prevFillStyle = nextFillStyle;
});
// flush the last color we encountered
if (prevFillStyle !== null) {
ctx.fillStyle = prevFillStyle;
this.fillCells(startX, startY, cols - startX, 1);
}
ctx.restore();
}
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
if (flags & FLAGS.INVISIBLE) {
return;
}
if (flags & FLAGS.UNDERLINE) {
this._ctx.save();
if (fg === INVERTED_DEFAULT_COLOR) {
this._ctx.fillStyle = this._colors.background.css;
} else if (fg < 256) {
// 256 color support
this._ctx.fillStyle = this._colors.ansi[fg].css;
} else {
this._ctx.fillStyle = this._colors.foreground.css;
}
this.fillBottomLineAtCells(x, y);
this._ctx.restore();
}
this.drawChar(
terminal, char, code,
width, x, y,
fg, bg,
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
);
});
}
public onGridChanged(terminal: ITerminal, firstRow: number, lastRow: number): void {
// Resize has not been called yet
if (this._state.cache.length === 0) {
return;
}
this._charAtlas.beginFrame();
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
this._drawBackground(terminal, firstRow, lastRow);
this._drawForeground(terminal, firstRow, lastRow);
}
public onOptionsChanged(terminal: ITerminal): void {
this.setTransparency(terminal, terminal.options.allowTransparency);
}
/**
* Whether a character is overlapping to the next cell.
*/
private _isOverlapping(charData: CharData): boolean {
// Only single cell characters can be overlapping, rendering issues can
// occur without this check
@@ -236,13 +263,13 @@ export class TextRenderLayer extends BaseRenderLayer {
* @param x The column of the char.
* @param y The row of the char.
*/
private _clearChar(x: number, y: number): void {
let colsToClear = 1;
// Clear the adjacent character if it was wide
const state = this._state.cache[x][y];
if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
colsToClear = 2;
}
this.clearCells(x, y, colsToClear, 1);
}
// private _clearChar(x: number, y: number): void {
// let colsToClear = 1;
// // Clear the adjacent character if it was wide
// const state = this._state.cache[x][y];
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
// colsToClear = 2;
// }
// this.clearCells(x, y, colsToClear, 1);
// }
}
+96 -4
View File
@@ -3,14 +3,106 @@
* @license MIT
*/
/**
import { ITerminal } from '../Types';
import { IEventEmitter, ITheme } from 'xterm';
import { IColorSet } from '../shared/Types';
/**
* Flags used to render terminal text properly.
*/
export enum FLAGS {
export const enum FLAGS {
BOLD = 1,
UNDERLINE = 2,
BLINK = 4,
INVERSE = 8,
INVISIBLE = 16,
DIM = 32
};
DIM = 32,
ITALIC = 64
}
export interface IRenderer extends IEventEmitter {
dimensions: IRenderDimensions;
colorManager: IColorManager;
setTheme(theme: ITheme): IColorSet;
onWindowResize(devicePixelRatio: number): void;
onResize(cols: number, rows: number): void;
onCharSizeChanged(): void;
onBlur(): void;
onFocus(): void;
onSelectionChanged(start: [number, number], end: [number, number]): void;
onCursorMove(): void;
onOptionsChanged(): void;
clear(): void;
refreshRows(start: number, end: number): void;
}
export interface IColorManager {
colors: IColorSet;
}
// TODO: We should probably rewrite the imports for IColorSet, but there's a lot of them
export { IColorSet };
export interface IRenderDimensions {
scaledCharWidth: number;
scaledCharHeight: number;
scaledCellWidth: number;
scaledCellHeight: number;
scaledCharLeft: number;
scaledCharTop: number;
scaledCanvasWidth: number;
scaledCanvasHeight: number;
canvasWidth: number;
canvasHeight: number;
actualCellWidth: number;
actualCellHeight: number;
}
export interface IRenderLayer {
/**
* Called when the terminal loses focus.
*/
onBlur(terminal: ITerminal): void;
/**
* * Called when the terminal gets focus.
*/
onFocus(terminal: ITerminal): void;
/**
* Called when the cursor is moved.
*/
onCursorMove(terminal: ITerminal): void;
/**
* Called when options change.
*/
onOptionsChanged(terminal: ITerminal): void;
/**
* Called when the theme changes.
*/
onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void;
/**
* Called when the data in the grid has changed (or needs to be rendered
* again).
*/
onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void;
/**
* Calls when the selection changes.
*/
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
/**
* Resize the render layer.
*/
resize(terminal: ITerminal, dim: IRenderDimensions): void;
/**
* Clear the state of the render layer.
*/
reset(terminal: ITerminal): void;
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IGlyphIdentifier } from './Types';
export default abstract class BaseCharAtlas {
private _didWarmUp: boolean = false;
/**
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
* Implement _doWarmUp instead if you only want to get called once.
*/
public warmUp(): void {
if (!this._didWarmUp) {
this._doWarmUp();
this._didWarmUp = true;
}
}
/**
* Perform any work needed to warm the cache before it can be used. Used by the default
* implementation of warmUp(), and will only be called once.
*/
protected _doWarmUp(): void { }
/**
* Called when we start drawing a new frame.
*
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
* of BaseRenderLayer.
*/
public beginFrame(): void { }
/**
* May be called before warmUp finishes, however it is okay for the implementation to
* do nothing and return false in that case.
*
* @param ctx Where to draw the character onto.
* @param glyph Information about what to draw
* @param x The position on the context to start drawing at
* @param y The position on the context to start drawing at
* @returns The success state. True if we drew the character.
*/
public abstract draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean;
}
+105
View File
@@ -0,0 +1,105 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { generateConfig, configEquals } from './CharAtlasUtils';
import BaseCharAtlas from './BaseCharAtlas';
import DynamicCharAtlas from './DynamicCharAtlas';
import NoneCharAtlas from './NoneCharAtlas';
import StaticCharAtlas from './StaticCharAtlas';
const charAtlasImplementations = {
'none': NoneCharAtlas,
'static': StaticCharAtlas,
'dynamic': DynamicCharAtlas
};
interface ICharAtlasCacheEntry {
atlas: BaseCharAtlas;
config: ICharAtlasConfig;
// N.B. This implementation potentially holds onto copies of the terminal forever, so
// this may cause memory leaks.
ownedBy: ITerminal[];
}
const charAtlasCache: ICharAtlasCacheEntry[] = [];
/**
* Acquires a char atlas, either generating a new one or returning an existing
* one that is in use by another terminal.
* @param terminal The terminal.
* @param colors The colors to use.
*/
export function acquireCharAtlas(
terminal: ITerminal,
colors: IColorSet,
scaledCharWidth: number,
scaledCharHeight: number
): BaseCharAtlas {
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
// TODO: Currently if a terminal changes configs it will not free the entry reference (until it's disposed)
// Check to see if the terminal already owns this config
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
const ownedByIndex = entry.ownedBy.indexOf(terminal);
if (ownedByIndex >= 0) {
if (configEquals(entry.config, newConfig)) {
return entry.atlas;
}
// The configs differ, release the terminal from the entry
if (entry.ownedBy.length === 1) {
charAtlasCache.splice(i, 1);
} else {
entry.ownedBy.splice(ownedByIndex, 1);
}
break;
}
}
// Try match a char atlas from the cache
for (let i = 0; i < charAtlasCache.length; i++) {
const entry = charAtlasCache[i];
if (configEquals(entry.config, newConfig)) {
// Add the terminal to the cache entry and return
entry.ownedBy.push(terminal);
return entry.atlas;
}
}
const newEntry: ICharAtlasCacheEntry = {
atlas: new charAtlasImplementations[terminal.options.experimentalCharAtlas](
document,
newConfig
),
config: newConfig,
ownedBy: [terminal]
};
charAtlasCache.push(newEntry);
return newEntry.atlas;
}
/**
* Removes a terminal reference from the cache, allowing its memory to be freed.
* @param terminal The terminal to remove.
*/
export function removeTerminalFromCache(terminal: ITerminal): void {
for (let i = 0; i < charAtlasCache.length; i++) {
const index = charAtlasCache[i].ownedBy.indexOf(terminal);
if (index !== -1) {
if (charAtlasCache[i].ownedBy.length === 1) {
// Remove the cache entry if it's the only terminal
charAtlasCache.splice(i, 1);
} else {
// Remove the reference from the cache entry
charAtlasCache[i].ownedBy.splice(index, 1);
}
break;
}
}
}
+53
View File
@@ -0,0 +1,53 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
const clonedColors = {
foreground: colors.foreground,
background: colors.background,
cursor: null,
cursorAccent: null,
selection: null,
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
// dynamic character atlas.
ansi: colors.ansi.slice(0, 16)
};
return {
type: terminal.options.experimentalCharAtlas,
devicePixelRatio: window.devicePixelRatio,
scaledCharWidth,
scaledCharHeight,
fontFamily: terminal.options.fontFamily,
fontSize: terminal.options.fontSize,
fontWeight: terminal.options.fontWeight,
fontWeightBold: terminal.options.fontWeightBold,
allowTransparency: terminal.options.allowTransparency,
colors: clonedColors
};
}
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
for (let i = 0; i < a.colors.ansi.length; i++) {
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
return false;
}
}
return a.type === b.type &&
a.devicePixelRatio === b.devicePixelRatio &&
a.fontFamily === b.fontFamily &&
a.fontSize === b.fontSize &&
a.fontWeight === b.fontWeight &&
a.fontWeightBold === b.fontWeightBold &&
a.allowTransparency === b.allowTransparency &&
a.scaledCharWidth === b.scaledCharWidth &&
a.scaledCharHeight === b.scaledCharHeight &&
a.colors.foreground === b.colors.foreground &&
a.colors.background === b.colors.background;
}
+242
View File
@@ -0,0 +1,242 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR } from './Types';
import { ICharAtlasConfig } from '../../shared/atlas/Types';
import { IColor } from '../../shared/Types';
import BaseCharAtlas from './BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
import { clearColor } from '../../shared/atlas/CharAtlasGenerator';
import LRUMap from './LRUMap';
// 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.
const TEXTURE_WIDTH = 1024;
const TEXTURE_HEIGHT = 1024;
const TRANSPARENT_COLOR = {
css: 'rgba(0, 0, 0, 0)',
rgba: 0
};
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
// frame ASAP.
//
// This helps to limit the amount of damage a program can do when it would otherwise thrash the
// cache.
const FRAME_CACHE_DRAW_LIMIT = 100;
interface IGlyphCacheValue {
index: number;
isEmpty: boolean;
}
function getGlyphCacheKey(glyph: IGlyphIdentifier): string {
const styleFlags = (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
return `${glyph.bg}_${glyph.fg}_${styleFlags}${glyph.char}`;
}
export default class DynamicCharAtlas extends BaseCharAtlas {
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
// It's ordered so that we can determine when to remove the old entries.
private _cacheMap: LRUMap<IGlyphCacheValue>;
// The texture that the atlas is drawn to
private _cacheCanvas: HTMLCanvasElement;
private _cacheCtx: CanvasRenderingContext2D;
// A temporary context that glyphs are drawn to before being transfered to the atlas.
private _tmpCtx: CanvasRenderingContext2D;
// The number of characters stored in the atlas by width/height
private _width: number;
private _height: number;
private _drawToCacheCount: number = 0;
constructor(document: Document, private _config: ICharAtlasConfig) {
super();
this._cacheCanvas = document.createElement('canvas');
this._cacheCanvas.width = TEXTURE_WIDTH;
this._cacheCanvas.height = TEXTURE_HEIGHT;
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this._cacheCtx = this._cacheCanvas.getContext('2d', {alpha: true});
const tmpCanvas = document.createElement('canvas');
tmpCanvas.width = this._config.scaledCharWidth;
tmpCanvas.height = this._config.scaledCharHeight;
this._tmpCtx = tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency});
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
const capacity = this._width * this._height;
this._cacheMap = new LRUMap(capacity);
this._cacheMap.prealloc(capacity);
// This is useful for debugging
// document.body.appendChild(this._cacheCanvas);
}
public beginFrame(): void {
this._drawToCacheCount = 0;
}
public draw(
ctx: CanvasRenderingContext2D,
glyph: IGlyphIdentifier,
x: number,
y: number
): boolean {
const glyphKey = getGlyphCacheKey(glyph);
const cacheValue = this._cacheMap.get(glyphKey);
if (cacheValue != null) {
this._drawFromCache(ctx, cacheValue, x, y);
return true;
} else if (this._canCache(glyph) && this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
let index;
if (this._cacheMap.size < this._cacheMap.capacity) {
index = this._cacheMap.size;
} else {
// we're out of space, so our call to set will delete this item
index = this._cacheMap.peek().index;
}
const cacheValue = this._drawToCache(glyph, index);
this._cacheMap.set(glyphKey, cacheValue);
this._drawFromCache(ctx, cacheValue, x, y);
return true;
}
return false;
}
private _canCache(glyph: IGlyphIdentifier): boolean {
// Only cache ascii and extended characters for now, to be safe. In the future, we could do
// something more complicated to determine the expected width of a character.
//
// If we switch the renderer over to webgl at some point, we may be able to use blending modes
// to draw overlapping glyphs from the atlas:
// https://github.com/servo/webrender/issues/464#issuecomment-255632875
// https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
return glyph.code < 256;
}
private _toCoordinates(index: number): [number, number] {
return [
(index % this._width) * this._config.scaledCharWidth,
Math.floor(index / this._width) * this._config.scaledCharHeight
];
}
private _drawFromCache(
ctx: CanvasRenderingContext2D,
cacheValue: IGlyphCacheValue,
x: number,
y: number
): void {
// We don't actually need to do anything if this is whitespace.
if (cacheValue.isEmpty) {
return;
}
const [cacheX, cacheY] = this._toCoordinates(cacheValue.index);
ctx.drawImage(
this._cacheCanvas,
cacheX,
cacheY,
this._config.scaledCharWidth,
this._config.scaledCharHeight,
x,
y,
this._config.scaledCharWidth,
this._config.scaledCharHeight
);
}
private _getColorFromAnsiIndex(idx: number): IColor {
if (idx < this._config.colors.ansi.length) {
return this._config.colors.ansi[idx];
}
return DEFAULT_ANSI_COLORS[idx];
}
private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
if (this._config.allowTransparency) {
// 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
// around the anti-aliased edges of the glyph, and it would look too dark.
return TRANSPARENT_COLOR;
} else if (glyph.bg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.foreground;
} else if (glyph.bg < 256) {
return this._getColorFromAnsiIndex(glyph.bg);
}
return this._config.colors.background;
}
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
return this._config.colors.background;
} else if (glyph.fg < 256) {
// 256 color support
return this._getColorFromAnsiIndex(glyph.fg);
}
return this._config.colors.foreground;
}
// TODO: We do this (or something similar) in multiple places. We should split this off
// into a shared function.
private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
this._drawToCacheCount++;
this._tmpCtx.save();
// draw the background
const backgroundColor = this._getBackgroundColor(glyph);
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
// transparency in backgroundColor
this._tmpCtx.globalCompositeOperation = 'copy';
this._tmpCtx.fillStyle = backgroundColor.css;
this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
this._tmpCtx.globalCompositeOperation = 'source-over';
// draw the foreground/glyph
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
const fontStyle = glyph.italic ? 'italic' : '';
this._tmpCtx.font =
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
this._tmpCtx.textBaseline = 'top';
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
// Apply alpha to dim the character
if (glyph.dim) {
this._tmpCtx.globalAlpha = DIM_OPACITY;
}
// Draw the character
this._tmpCtx.fillText(glyph.char, 0, 0);
this._tmpCtx.restore();
// clear the background from the character to avoid issues with drawing over the previous
// character if it extends past it's bounds
const imageData = this._tmpCtx.getImageData(
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
);
let isEmpty = false;
if (!this._config.allowTransparency) {
isEmpty = clearColor(imageData, backgroundColor);
}
// copy the data from imageData to _cacheCanvas
const [x, y] = this._toCoordinates(index);
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
this._cacheCtx.putImageData(imageData, x, y);
return {
index,
isEmpty
};
}
}
+65
View File
@@ -0,0 +1,65 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import LRUMap from './LRUMap';
describe('LRUMap', () => {
it('can be used to store and retrieve values', () => {
const map = new LRUMap(10);
map.set('keya', 'valuea');
map.set('keyb', 'valueb');
map.set('keyc', 'valuec');
assert.strictEqual(map.get('keya'), 'valuea');
assert.strictEqual(map.get('keyb'), 'valueb');
assert.strictEqual(map.get('keyc'), 'valuec');
});
it('maintains a size from insertions', () => {
const map = new LRUMap(10);
assert.strictEqual(map.size, 0);
map.set('a', 'value');
assert.strictEqual(map.size, 1);
map.set('b', 'value');
assert.strictEqual(map.size, 2);
});
it('deletes the oldest entry when the capacity is exceeded', () => {
const map = new LRUMap(4);
map.set('a', 'value');
map.set('b', 'value');
map.set('c', 'value');
map.set('d', 'value');
map.set('e', 'value');
assert.isNull(map.get('a'));
assert.isNotNull(map.get('b'));
assert.isNotNull(map.get('c'));
assert.isNotNull(map.get('d'));
assert.isNotNull(map.get('e'));
assert.strictEqual(map.size, 4);
});
it('prevents a recently accessed entry from getting deleted', () => {
const map = new LRUMap(2);
map.set('a', 'value');
map.set('b', 'value');
map.get('a');
// a would normally get deleted here, except that we called get()
map.set('c', 'value');
assert.isNotNull(map.get('a'));
// b got deleted instead of a
assert.isNull(map.get('b'));
assert.isNotNull(map.get('c'));
});
it('supports mutation', () => {
const map = new LRUMap(10);
map.set('keya', 'oldvalue');
map.set('keya', 'newvalue');
// mutation doesn't change the size
assert.strictEqual(map.size, 1);
assert.strictEqual(map.get('keya'), 'newvalue');
});
});
+125
View File
@@ -0,0 +1,125 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
interface ILinkedListNode<T> {
prev: ILinkedListNode<T>;
next: ILinkedListNode<T>;
key: string;
value: T;
}
export default class LRUMap<T> {
private _map = {};
private _head: ILinkedListNode<T> = null;
private _tail: ILinkedListNode<T> = null;
private _nodePool: ILinkedListNode<T>[] = [];
public size: number = 0;
constructor(public capacity: number) { }
private _unlinkNode(node: ILinkedListNode<T>): void {
const prev = node.prev;
const next = node.next;
if (node === this._head) {
this._head = next;
}
if (node === this._tail) {
this._tail = prev;
}
if (prev !== null) {
prev.next = next;
}
if (next !== null) {
next.prev = prev;
}
}
private _appendNode(node: ILinkedListNode<T>): void {
const tail = this._tail;
if (tail !== null) {
tail.next = node;
}
node.prev = tail;
node.next = null;
this._tail = node;
if (this._head === null) {
this._head = node;
}
}
/**
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
* they're more likely to live next to each other in memory, which seems to improve performance.
*
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
* large maps.
*/
public prealloc(count: number): void {
const nodePool = this._nodePool;
for (let i = 0; i < count; i++) {
nodePool.push({
prev: null,
next: null,
key: null,
value: null
});
}
}
public get(key: string): T | null {
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
const node = this._map[key];
if (node !== undefined) {
this._unlinkNode(node);
this._appendNode(node);
return node.value;
}
return null;
}
public peek(): T | null {
const head = this._head;
return head === null ? null : head.value;
}
public set(key: string, value: T): void {
// This is unsafe: See note above.
let node = this._map[key];
if (node !== undefined) {
// already exists, we just need to mutate it and move it to the end of the list
node = this._map[key];
this._unlinkNode(node);
node.value = value;
} else if (this.size >= this.capacity) {
// we're out of space: recycle the head node, move it to the tail
node = this._head;
this._unlinkNode(node);
delete this._map[node.key];
node.key = key;
node.value = value;
this._map[key] = node;
} else {
// make a new element
const nodePool = this._nodePool;
if (nodePool.length > 0) {
// use a preallocated node if we can
node = nodePool.pop();
node.key = key;
node.value = value;
} else {
node = {
prev: null,
next: null,
key,
value
};
}
this._map[key] = node;
this.size++;
}
this._appendNode(node);
}
}

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