Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f35c21355 | ||
|
|
13133d699f | ||
|
|
0ed75dc100 | ||
|
|
b3a631750d | ||
|
|
9643de4c01 | ||
|
|
ab1263f669 | ||
|
|
9ebe094a2f | ||
|
|
0c3f2d7222 | ||
|
|
a39da0f0d5 | ||
|
|
adac12eded | ||
|
|
030fb78712 | ||
|
|
96b3eb982b | ||
|
|
5276ad8146 | ||
|
|
4c42393aa4 | ||
|
|
1942b4ad51 | ||
|
|
05ca1cb7dc | ||
|
|
2f4c5b93fa | ||
|
|
de8d9a720a | ||
|
|
7c2bc70aac | ||
|
|
44e01ba006 | ||
|
|
8ffdc14143 | ||
|
|
fda964b3ea | ||
|
|
b748eca980 | ||
|
|
bbefedb6eb | ||
|
|
c387180a47 | ||
|
|
0e1704f563 | ||
|
|
b8f55f1c8f | ||
|
|
a428193424 | ||
|
|
1feb0a27e9 | ||
|
|
a92e4adfe2 | ||
|
|
7716e9abda | ||
|
|
6e196da59c | ||
|
|
69e8f62149 | ||
|
|
b46757010f | ||
|
|
a74173adf2 | ||
|
|
c10b18113e | ||
|
|
7786e59c77 | ||
|
|
9dfac7e674 | ||
|
|
922e594fcf | ||
|
|
c73aa07135 | ||
|
|
acea979fe0 | ||
|
|
e4f626e3b6 | ||
|
|
9a4f419820 | ||
|
|
209c44898d | ||
|
|
d3ff32da72 | ||
|
|
f81d1cb1cf | ||
|
|
8f09f58119 | ||
|
|
ab0eedc513 | ||
|
|
8e460dfe55 | ||
|
|
37ed1e4796 | ||
|
|
d0a3164e99 | ||
|
|
968dd93ba7 | ||
|
|
739723fe92 | ||
|
|
2f08b87d95 | ||
|
|
eb70ddcba5 | ||
|
|
43015f85f4 | ||
|
|
87dca56dee | ||
|
|
72e6863a7f | ||
|
|
e83aa64794 | ||
|
|
c7416a6eaa | ||
|
|
9ed5920275 | ||
|
|
497d31bec3 | ||
|
|
24de302d2f | ||
|
|
5f70b3f3be | ||
|
|
b64cc0a9de | ||
|
|
3e4c1b5a21 | ||
|
|
f698cafccb | ||
|
|
8792bb5f36 | ||
|
|
ac48ef977b | ||
|
|
4119c7d1d7 | ||
|
|
000fefdead | ||
|
|
33955398e7 | ||
|
|
dbafc793da | ||
|
|
88c1ff6f11 | ||
|
|
610c4c9726 | ||
|
|
e2214b0239 | ||
|
|
23d3a27a6f | ||
|
|
d04016d51f | ||
|
|
fede301f5f | ||
|
|
a62ec679bd | ||
|
|
da79fec5c2 | ||
|
|
80025804b3 | ||
|
|
955c45906b | ||
|
|
f331269ef4 | ||
|
|
9cb6706e2d | ||
|
|
b9cfbbea8f | ||
|
|
58d461bb90 | ||
|
|
69411916cb | ||
|
|
1a596f9608 | ||
|
|
67be8c36e7 | ||
|
|
0b50c06810 | ||
|
|
53167df1a8 | ||
|
|
eef855601b | ||
|
|
6fd5cdd8f1 | ||
|
|
c0e9bbe87f | ||
|
|
06c292c11d | ||
|
|
bda068c5f0 | ||
|
|
031a30b007 | ||
|
|
3475764946 | ||
|
|
f90239377b | ||
|
|
1502fea431 | ||
|
|
caf8f9d35e | ||
|
|
d230f93bde | ||
|
|
bae125b789 | ||
|
|
eda04bcb23 | ||
|
|
523d562f73 | ||
|
|
0c588a7333 | ||
|
|
f7612a49d2 | ||
|
|
34244248bf | ||
|
|
f654044e5e | ||
|
|
a7fa58cc60 | ||
|
|
9ca9f7296e | ||
|
|
5bbbbf7348 | ||
|
|
da7c39e526 | ||
|
|
6674f05cc8 | ||
|
|
161c7f9339 | ||
|
|
780e924b21 | ||
|
|
1f000054c2 | ||
|
|
c13d6e5eeb | ||
|
|
e5dfc56030 | ||
|
|
00423407c1 | ||
|
|
2340fcd071 | ||
|
|
59a1749d44 | ||
|
|
0fae9bb5a8 | ||
|
|
70ba349a85 | ||
|
|
49574be510 | ||
|
|
f41e00ed97 | ||
|
|
ce079f399c | ||
|
|
7a567d2717 | ||
|
|
cd3f2609d6 | ||
|
|
8a929ed142 | ||
|
|
46995fdbc1 | ||
|
|
d694eae6b5 | ||
|
|
bce2eae66f | ||
|
|
3554d3e075 | ||
|
|
eb6deb67f9 | ||
|
|
7c55e1be9e | ||
|
|
7549ca59be | ||
|
|
af81acc50e | ||
|
|
f033800554 | ||
|
|
623dd56190 | ||
|
|
740604f035 | ||
|
|
3f68a60e73 | ||
|
|
556c56ff94 | ||
|
|
dfb5d8e663 | ||
|
|
7562948259 | ||
|
|
34ac1bc502 | ||
|
|
97b176e701 | ||
|
|
8a5b6b0356 | ||
|
|
55d590348c | ||
|
|
23bdb2a6c2 | ||
|
|
16aaf6c58c | ||
|
|
22c9ad7145 | ||
|
|
41a7cabbc2 | ||
|
|
724baf22f9 | ||
|
|
c04ebe974f | ||
|
|
fcee890819 | ||
|
|
d696046921 | ||
|
|
518e2734ba | ||
|
|
eedcbccb15 | ||
|
|
d7e6cb17a5 | ||
|
|
b658baf921 | ||
|
|
10c6e900ad | ||
|
|
b608c9d0ef | ||
|
|
0e0bcde957 | ||
|
|
d735196a30 | ||
|
|
59aec69379 | ||
|
|
228ec7b818 | ||
|
|
7b7fc53157 | ||
|
|
f381cdfc33 | ||
|
|
ac6bced6e7 | ||
|
|
1a61401cbe | ||
|
|
6d55ab23fd | ||
|
|
3abc090412 | ||
|
|
21f1498977 | ||
|
|
16c0550d7c | ||
|
|
517d5cfd0e | ||
|
|
d363416fb4 | ||
|
|
ffc868a4b8 | ||
|
|
e40f924913 | ||
|
|
ca1c647442 | ||
|
|
79e182d63f | ||
|
|
c1b56722d3 | ||
|
|
b959fc8d6a | ||
|
|
48753625f3 | ||
|
|
dc236aeb20 | ||
|
|
71886167be | ||
|
|
4b1c54d634 | ||
|
|
2d0937ac1d | ||
|
|
0d5cd9582f | ||
|
|
e831bdf1ef | ||
|
|
4729bfaa2d | ||
|
|
a88469aac6 | ||
|
|
d6da010c7e | ||
|
|
c6138edec2 | ||
|
|
15a6c9846d | ||
|
|
ca193ed00d | ||
|
|
ffcdba8d5b | ||
|
|
d68c441b86 | ||
|
|
613f877954 | ||
|
|
a1353a8fb4 | ||
|
|
4269777f1c | ||
|
|
23eed1b3ea | ||
|
|
ebc0083531 | ||
|
|
7d3e2d7da4 | ||
|
|
ea09c6af09 | ||
|
|
267357c4de | ||
|
|
f416bd155a | ||
|
|
c6a3fb3b42 | ||
|
|
cb33c897d2 | ||
|
|
469d5158a0 | ||
|
|
766b1745d3 | ||
|
|
68182ed162 | ||
|
|
d261862ccb | ||
|
|
8d6682a2bc | ||
|
|
63833b59ed | ||
|
|
4fd9b3f249 | ||
|
|
3f145b8a0c | ||
|
|
1f42e7c06a | ||
|
|
42b1dca7db | ||
|
|
f0c3bab156 | ||
|
|
4453a1cbf7 | ||
|
|
362152004b | ||
|
|
88d5cf46f8 | ||
|
|
ebfa57b71c | ||
|
|
4c3082c924 | ||
|
|
672db9437f | ||
|
|
eb10652e46 | ||
|
|
f4bbfc30f3 | ||
|
|
65eaea39d3 | ||
|
|
70db48b533 | ||
|
|
779ebe87e5 | ||
|
|
6237eaa86f | ||
|
|
736bd53e49 | ||
|
|
69d43d4a40 | ||
|
|
017a5b1218 | ||
|
|
c6f785f51a | ||
|
|
8718d3a4ca | ||
|
|
7aaa1ad0bb | ||
|
|
fa70aa0983 | ||
|
|
89a3931a68 | ||
|
|
a0ee09a7bc | ||
|
|
94415a227d | ||
|
|
0b4ff4bb6b | ||
|
|
4be376d397 | ||
|
|
5362ea195c | ||
|
|
b55bedc6af | ||
|
|
cf4b59567b | ||
|
|
b9bc219ecc | ||
|
|
f4c7e3fd7d | ||
|
|
36567e60df | ||
|
|
2e0e24c382 | ||
|
|
290bde6163 | ||
|
|
db78b75e8b | ||
|
|
87328fe293 | ||
|
|
2c96c262f4 | ||
|
|
f111aa27dc | ||
|
|
8e5d372b81 | ||
|
|
8b5b64605e | ||
|
|
0c388c69bc | ||
|
|
660466d244 | ||
|
|
dee4429151 | ||
|
|
c3d1b0703c | ||
|
|
71827f7981 | ||
|
|
2d80abe801 | ||
|
|
6b0f2170ea | ||
|
|
a86c2943b8 | ||
|
|
cb591f212a | ||
|
|
e5f94391ae | ||
|
|
5476f4cb93 | ||
|
|
aaf8b80292 | ||
|
|
9ab3d319d1 | ||
|
|
efeca49705 | ||
|
|
267071ead6 | ||
|
|
26a80d0d35 | ||
|
|
49f58f5d25 | ||
|
|
a0b4ee56f6 | ||
|
|
6ff81c43f6 | ||
|
|
625e168e1b | ||
|
|
b2ab89ddaa | ||
|
|
4ace81d994 | ||
|
|
25745777c6 | ||
|
|
74b68ac1c7 | ||
|
|
2a4208cf3e | ||
|
|
e58033bbac | ||
|
|
bd92aaf4dc | ||
|
|
3ac313f8e1 | ||
|
|
ab44650d87 | ||
|
|
11ab4dbd5b | ||
|
|
92c435a16d | ||
|
|
ddd02b4968 | ||
|
|
7e36bf47c6 | ||
|
|
d6daf40250 | ||
|
|
09a5fd4e61 | ||
|
|
171b9337cb | ||
|
|
bb324ba9c1 | ||
|
|
27283e45e9 | ||
|
|
23ce6ce19e | ||
|
|
e8a9994583 | ||
|
|
46f3b3fa62 | ||
|
|
2f8d49c74d | ||
|
|
629238ae70 | ||
|
|
3f9d90ecf6 | ||
|
|
f35dfb496a | ||
|
|
4e6f4be6cc | ||
|
|
798cf8abc3 | ||
|
|
b591db8c02 | ||
|
|
b18829963c | ||
|
|
e402dba635 | ||
|
|
2023b5b677 | ||
|
|
68e3d7fa16 | ||
|
|
3f993d675f | ||
|
|
7ddcf1a2a2 | ||
|
|
e6333bde2d | ||
|
|
51c7337099 | ||
|
|
6476429077 | ||
|
|
7b9a718af0 | ||
|
|
60113fa155 | ||
|
|
60c2b20d68 | ||
|
|
f8fb4da9f4 | ||
|
|
5f014be057 | ||
|
|
b12d2c218e | ||
|
|
ba4662ac14 | ||
|
|
ca1f7f1392 | ||
|
|
fa505111a2 | ||
|
|
2e10c8c25e | ||
|
|
8d4da3c2ac | ||
|
|
81dda13218 | ||
|
|
0a8d57ce74 | ||
|
|
5c8c680dac | ||
|
|
69f8700557 | ||
|
|
4a3609f805 | ||
|
|
fa2cbb0aca | ||
|
|
b05c66b132 | ||
|
|
75046235af | ||
|
|
bab94ebbdc | ||
|
|
4d008c66f1 | ||
|
|
bf9d879efa | ||
|
|
636afa11d6 | ||
|
|
1283123d4d | ||
|
|
e182e3d4e4 | ||
|
|
51033596ef | ||
|
|
0960a7ae14 | ||
|
|
84d5963e7e | ||
|
|
10b7b467b4 | ||
|
|
2317a319d0 | ||
|
|
f80924fb8d | ||
|
|
18f4dc6b3e | ||
|
|
b400f9ca3b | ||
|
|
0b78011fa3 | ||
|
|
2be0a5819b | ||
|
|
e346f8bba3 | ||
|
|
1451bcb6ae | ||
|
|
f1ef9abc77 | ||
|
|
f9df863ee0 | ||
|
|
e6ac5634bb | ||
|
|
4dbe8ec1db | ||
|
|
8b6d564598 | ||
|
|
131e4f78bd | ||
|
|
99beeb2270 | ||
|
|
0d63cecc6b | ||
|
|
3c444ca39a | ||
|
|
03e648a0fb | ||
|
|
48a98fa29f | ||
|
|
27ebe8c353 | ||
|
|
62cfa642cb | ||
|
|
c16e3e54e8 | ||
|
|
bc86a61a0c | ||
|
|
19d36f9228 | ||
|
|
b4bef0986c | ||
|
|
f63bd3d218 | ||
|
|
5cf2a21ddd | ||
|
|
7e1b5cc7e9 | ||
|
|
4046d682c9 | ||
|
|
127a2ca27a | ||
|
|
525af9c36a | ||
|
|
5878aa099d | ||
|
|
9597ad20d3 | ||
|
|
6deb3e13d2 | ||
|
|
ecb7f429bc | ||
|
|
ce09ae4ff8 | ||
|
|
bc41cc7d27 | ||
|
|
51e2cdfafb | ||
|
|
f41146fb40 | ||
|
|
8fa7f177a6 | ||
|
|
07430a4892 | ||
|
|
ce1c45fb18 | ||
|
|
55f3aeeff5 | ||
|
|
a99364eae9 | ||
|
|
7b41b002c6 | ||
|
|
32e157bfaa | ||
|
|
655c595011 | ||
|
|
064a372c71 | ||
|
|
c63f15a9b2 | ||
|
|
1aa40eb58b | ||
|
|
d8b973b126 | ||
|
|
05b162eefd | ||
|
|
46aa6a40f9 | ||
|
|
9f2aec1944 | ||
|
|
42b66d98b2 | ||
|
|
1eee182860 | ||
|
|
bd602fb078 | ||
|
|
8ea254bdc7 | ||
|
|
09754fddf8 | ||
|
|
a8a0344d1a | ||
|
|
4e479b455a | ||
|
|
e178139907 | ||
|
|
0854f84653 | ||
|
|
a35cd43395 | ||
|
|
3285374618 | ||
|
|
6ad6a01c3f | ||
|
|
e1c1c7a4f2 | ||
|
|
437a59d4cd | ||
|
|
9f29eeed23 | ||
|
|
b0f01beb0f | ||
|
|
857ae4b702 | ||
|
|
c934200c86 | ||
|
|
78426d8a12 | ||
|
|
817401bbcd | ||
|
|
84d7bfeacc | ||
|
|
92be01dd81 | ||
|
|
d17dfbc73f | ||
|
|
ba9ed43acd | ||
|
|
b88230db8b | ||
|
|
69d3a4667f | ||
|
|
abf6a9fa58 | ||
|
|
ea9d1d7ef0 | ||
|
|
f3aac10bbd | ||
|
|
f30012b985 | ||
|
|
db9aa5c88b | ||
|
|
1829cb7609 | ||
|
|
6d6be9bed8 | ||
|
|
54a1319f57 | ||
|
|
65dbf88460 | ||
|
|
0c54b1a29e | ||
|
|
179e5d5892 | ||
|
|
6993daef15 | ||
|
|
801db3831d | ||
|
|
e813ae02fc | ||
|
|
73ec8a89a0 | ||
|
|
c2446df966 | ||
|
|
2de39024be | ||
|
|
079729f1b4 | ||
|
|
ec3a95f727 | ||
|
|
c4a5e0862c | ||
|
|
594797e05b | ||
|
|
682efc7206 | ||
|
|
9791d8ab49 | ||
|
|
8e88ce8da5 | ||
|
|
828f14f2ab | ||
|
|
7b7f85e3cf | ||
|
|
7da40b9d56 | ||
|
|
a1b61e05f9 | ||
|
|
21cb9872f3 | ||
|
|
93d3842156 | ||
|
|
db31a44da3 | ||
|
|
33e46682b1 | ||
|
|
fc1692b3eb | ||
|
|
d48982b60e | ||
|
|
9c72254d3a | ||
|
|
af6544d6d9 | ||
|
|
39fd6c427d | ||
|
|
ef3c0c0e72 | ||
|
|
13f0fd0503 | ||
|
|
c60f9b1468 | ||
|
|
e77e36cbb7 | ||
|
|
b20730ee4d | ||
|
|
acea73c8ac | ||
|
|
cc531f1933 | ||
|
|
c1b0c9bbf1 | ||
|
|
358d70d721 | ||
|
|
975dfc4a5e | ||
|
|
fdf784ea96 | ||
|
|
e67bef69f4 | ||
|
|
8b6327b563 | ||
|
|
8e37883b90 | ||
|
|
ace855d0fe | ||
|
|
007cac22d2 | ||
|
|
b047045949 | ||
|
|
c7cfca395c | ||
|
|
a7f2348c63 | ||
|
|
51a148508e | ||
|
|
90a41a113a | ||
|
|
2a1f25e7dc | ||
|
|
7e3b315d91 | ||
|
|
3a32a5714c | ||
|
|
273cc45357 | ||
|
|
4dab31dbe0 | ||
|
|
87e9789748 | ||
|
|
4855b60ffe | ||
|
|
21165a4286 | ||
|
|
c908da351b | ||
|
|
52a429f584 | ||
|
|
e6447bc91f | ||
|
|
b587c2f30f | ||
|
|
4f531f8490 | ||
|
|
19931e75c0 | ||
|
|
2edf02e468 | ||
|
|
e1363e9fe7 | ||
|
|
109e3a50e8 | ||
|
|
99ac78031c | ||
|
|
d4bd8ae2be | ||
|
|
dfff04cf3d | ||
|
|
deb04ca162 | ||
|
|
178c513407 | ||
|
|
df7cd9c319 | ||
|
|
ce69dceee7 | ||
|
|
4843ca5bb8 | ||
|
|
8bdb606fd1 | ||
|
|
e6e5ecc0f2 | ||
|
|
3a2076805c | ||
|
|
d7ea0edfcf | ||
|
|
50abb43e9c | ||
|
|
6bdd380890 | ||
|
|
07d3407892 | ||
|
|
68197ed47b | ||
|
|
b13b737a07 | ||
|
|
200f7bf33d | ||
|
|
9549896ea5 | ||
|
|
2370038523 | ||
|
|
df739ab814 | ||
|
|
8d850d6c8b | ||
|
|
ae50d84c5a | ||
|
|
daaed5ac8f | ||
|
|
e02a9ff09e | ||
|
|
1536e79542 | ||
|
|
94c19fb92a | ||
|
|
9cf479bcfa | ||
|
|
0802582c9b | ||
|
|
3515c0b220 | ||
|
|
70ded42bee | ||
|
|
c444eebd89 | ||
|
|
7c6dad5805 | ||
|
|
40b231edd1 | ||
|
|
35a6aa8326 | ||
|
|
711ae65947 | ||
|
|
ece7192db7 | ||
|
|
3e456971b1 | ||
|
|
60a591e8c8 | ||
|
|
56dec2849f | ||
|
|
fe6919b52a | ||
|
|
4bf7f3eb89 | ||
|
|
523ff6e5fc | ||
|
|
88a037b309 | ||
|
|
ad67f47acb | ||
|
|
329d40ab14 | ||
|
|
a1f8749411 | ||
|
|
e7e0f67ea8 | ||
|
|
907a6b9732 | ||
|
|
7fe3f0a4e3 | ||
|
|
9236934586 | ||
|
|
932d5c7c21 | ||
|
|
509ce5fa3a | ||
|
|
fed430b78c | ||
|
|
883ad01bd5 | ||
|
|
249f8800af | ||
|
|
0b8c320579 | ||
|
|
f04f2efc52 | ||
|
|
f090fa822d | ||
|
|
9bcf06f658 | ||
|
|
6581eb7d88 | ||
|
|
90950e2b00 | ||
|
|
62dfa84d5d | ||
|
|
b93b774971 | ||
|
|
6552a023c1 | ||
|
|
13ffed392d | ||
|
|
d7e5977d9a | ||
|
|
7a416aa18f | ||
|
|
7ba17ca770 | ||
|
|
290394b401 | ||
|
|
802543f7cf | ||
|
|
f3619ab61c | ||
|
|
2f66efa49a | ||
|
|
75fbda1fe6 | ||
|
|
b0eecea45a | ||
|
|
f63223f8d8 | ||
|
|
a2a8c3d47f | ||
|
|
f98572cf71 | ||
|
|
ba1582cf3c | ||
|
|
c668bb1243 | ||
|
|
b4b6bab000 | ||
|
|
c784a012c7 | ||
|
|
efe22d8cf3 | ||
|
|
27b91f6b00 | ||
|
|
1fb1f556b1 | ||
|
|
8fc66a39e3 | ||
|
|
740ca9ce6d | ||
|
|
2918a6763f | ||
|
|
dfc853aef4 | ||
|
|
4db95f253e | ||
|
|
805c02fb18 | ||
|
|
d120ef5301 | ||
|
|
68fbb2f8c9 | ||
|
|
515cf7878e | ||
|
|
a35ad6aa81 | ||
|
|
aa2d28e619 | ||
|
|
a6c3f94860 | ||
|
|
ff81ce078b | ||
|
|
5fd764a89d | ||
|
|
83c734eae3 | ||
|
|
570cd786cb | ||
|
|
29ff74e069 | ||
|
|
c66a24fab4 | ||
|
|
7a579a374a | ||
|
|
f534758e29 | ||
|
|
dbd9b2af37 | ||
|
|
68b2b79880 | ||
|
|
8fbeadd5f9 | ||
|
|
f961f90bc8 | ||
|
|
338558d86c | ||
|
|
8b3513f009 | ||
|
|
9b0b323957 | ||
|
|
48c1d36d77 | ||
|
|
2ce67b89bc | ||
|
|
6559931f34 | ||
|
|
840970eac7 | ||
|
|
40e8618cf5 | ||
|
|
db488ebcc9 | ||
|
|
1612cec3e0 | ||
|
|
135e31f2ca | ||
|
|
b7081abfce | ||
|
|
c9f4a650c6 | ||
|
|
478742a22e | ||
|
|
bbfe149397 | ||
|
|
d94c897754 | ||
|
|
dde96187fa | ||
|
|
4a9f10d062 | ||
|
|
7435971c32 | ||
|
|
1b68682b79 | ||
|
|
565cba9c1a | ||
|
|
4375b3cdb9 | ||
|
|
72369e060e | ||
|
|
2a0da173f2 | ||
|
|
a33e8a6af7 | ||
|
|
913150fe13 | ||
|
|
358898daf9 | ||
|
|
7684f93773 | ||
|
|
ae29dbb133 | ||
|
|
3311ed509a | ||
|
|
8bc04c2fa7 | ||
|
|
fa47036982 | ||
|
|
314f98f2a6 | ||
|
|
8a50729a98 | ||
|
|
c133c644a6 | ||
|
|
951f281bb0 | ||
|
|
d5cacbaf4a | ||
|
|
bb198a980c | ||
|
|
60bc1bb873 | ||
|
|
d5bffb5395 | ||
|
|
2611a9b1d1 | ||
|
|
689ac3e073 | ||
|
|
51e1f49bf3 | ||
|
|
838d727a89 | ||
|
|
9bcfb4d722 | ||
|
|
0d9487c10f | ||
|
|
b48f9108c8 | ||
|
|
d698faa183 | ||
|
|
1a1f7e984f | ||
|
|
e0d535e0da | ||
|
|
76302e2146 | ||
|
|
204e42a151 | ||
|
|
bb9732599b | ||
|
|
adbb929f78 | ||
|
|
c045c803d6 | ||
|
|
38796a0f74 | ||
|
|
d01efdda27 | ||
|
|
9e3e724f63 | ||
|
|
a80baa71e8 | ||
|
|
272cb08472 | ||
|
|
48ff841d6a | ||
|
|
0e8a102e17 | ||
|
|
c6e6c51929 | ||
|
|
81ada39989 | ||
|
|
78e7e39bb4 | ||
|
|
548d8cd764 | ||
|
|
a4b4d082fb | ||
|
|
29cc0bf933 | ||
|
|
2896f56ebb | ||
|
|
fcef4d5d28 | ||
|
|
701f50f47b | ||
|
|
61d0e99ffd | ||
|
|
3eac446a85 | ||
|
|
71d8a4d1ed | ||
|
|
ff73f705ae | ||
|
|
64dd9d46fb | ||
|
|
0782fbb828 | ||
|
|
b820f23da6 | ||
|
|
ec1060187d | ||
|
|
1104bcbccb | ||
|
|
90ec90746e | ||
|
|
b39e8cfbb9 | ||
|
|
5ebd543b8b | ||
|
|
86f194a6d3 | ||
|
|
9cd7bf2af1 | ||
|
|
c961175c03 | ||
|
|
04ac36fc16 | ||
|
|
d27b43d515 | ||
|
|
0da6300102 | ||
|
|
e5ff04e798 | ||
|
|
5460068d52 | ||
|
|
a53c6f1728 | ||
|
|
6a5b5c23f8 | ||
|
|
ef171bf29f | ||
|
|
6b65ebd4aa | ||
|
|
8a5a03238f | ||
|
|
4feed2dabc | ||
|
|
415e760a8a | ||
|
|
3927364f0e | ||
|
|
62be3047d8 | ||
|
|
d18c947854 | ||
|
|
5af4626ec7 | ||
|
|
4446fa34de | ||
|
|
4a7a19c368 | ||
|
|
6351a5b748 | ||
|
|
dc5c2381a8 | ||
|
|
551feb849e | ||
|
|
98285d3a84 | ||
|
|
98ccc23934 | ||
|
|
d0d94584ad | ||
|
|
e00c7d535d | ||
|
|
8ceea112f7 | ||
|
|
53fd04a886 | ||
|
|
ffb2708a81 | ||
|
|
8ad2d1bf43 | ||
|
|
a758f8c82f | ||
|
|
bbbfc04fa5 | ||
|
|
d7f284c676 | ||
|
|
943def1391 | ||
|
|
a0a8003d60 | ||
|
|
6cc0a1447d | ||
|
|
4d3345182f | ||
|
|
3a5d07a8f2 | ||
|
|
1e94fce545 | ||
|
|
da8546725f | ||
|
|
53f3b4e368 | ||
|
|
a0d583b075 | ||
|
|
9005de1c42 | ||
|
|
b9a22eb468 | ||
|
|
b51c07c408 | ||
|
|
3397ecf977 | ||
|
|
801dcc3b75 | ||
|
|
91ef7f24a6 | ||
|
|
65877d491e | ||
|
|
f34c3d509d | ||
|
|
e1b3c7233a | ||
|
|
08dc8a3e5f | ||
|
|
6a2e6a5edc | ||
|
|
2d8f420db8 | ||
|
|
2cb293ceb2 | ||
|
|
3d2ae2b01e | ||
|
|
1c4a71f0df | ||
|
|
30a667c3be | ||
|
|
03115639a9 | ||
|
|
48629e7eb2 | ||
|
|
6b9fb48898 | ||
|
|
fe9eb7d31e | ||
|
|
693e17a5cb | ||
|
|
b5b2bcd0c9 | ||
|
|
fd34caee23 | ||
|
|
278d696709 | ||
|
|
7f18199985 | ||
|
|
b68974517e | ||
|
|
4d660deff3 | ||
|
|
38adfcd81c | ||
|
|
376c790e1d | ||
|
|
9d4beb024a | ||
|
|
99e4a9649f | ||
|
|
eb27641928 | ||
|
|
2483a98f63 | ||
|
|
5d3ef759b4 | ||
|
|
a6ef64dcb6 | ||
|
|
a4e3646640 | ||
|
|
591dfd506d | ||
|
|
14d6960873 | ||
|
|
06e89549a8 | ||
|
|
3ca0598279 | ||
|
|
3d2035e1a0 | ||
|
|
a15b866013 | ||
|
|
2f9c04c3af | ||
|
|
67df9a5cdc | ||
|
|
5395503031 | ||
|
|
200b59b47c | ||
|
|
cabd4a721c | ||
|
|
745e228e98 | ||
|
|
8ffcd65cff | ||
|
|
1c111d3e60 | ||
|
|
62f1d64af4 | ||
|
|
ee6fa6ca00 |
@@ -0,0 +1,22 @@
|
||||
FROM node:10
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install --no-install-recommends apt-utils 2>&1
|
||||
|
||||
# Verify git and process tools are installed
|
||||
RUN apt-get install -y git procps
|
||||
|
||||
# Install yarn
|
||||
RUN apt-get install -y curl apt-transport-https lsb-release \
|
||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends yarn
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"dockerFile": "Dockerfile",
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"editorconfig.editorconfig",
|
||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Question
|
||||
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
|
||||
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs
|
||||
---
|
||||
|
||||
🛑 The issue tracker is not for questions 🛑
|
||||
|
||||
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
|
||||
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs.
|
||||
|
||||
@@ -12,7 +12,6 @@ npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
@@ -42,6 +42,7 @@ lib/test/
|
||||
docs/
|
||||
/.idea/
|
||||
.vscode/
|
||||
bin/
|
||||
build/
|
||||
fixtures/
|
||||
coverage/
|
||||
|
||||
Vendored
+63
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Unit Tests",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--colors",
|
||||
"--recursive",
|
||||
"${workspaceRoot}/lib/**/*.test.js"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Integration Tests",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--colors",
|
||||
"--recursive",
|
||||
"${workspaceRoot}/lib/**/*.api.js"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"type": "chrome",
|
||||
"request": "launch",
|
||||
"name": "Demo Client",
|
||||
"url": "http://127.0.0.1:3000",
|
||||
"webRoot": "${workspaceFolder}/"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Demo Server",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"start-debug"
|
||||
],
|
||||
"port": 9229,
|
||||
"serverReadyAction": {
|
||||
"action": "openExternally",
|
||||
"pattern": "App listening to (http://.*?:[0-9]+)"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
}
|
||||
Vendored
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"group": "test",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": ["npm: watch", "npm: start"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
List of xterm.js contributors. Updated before every release.
|
||||
|
||||
7PH <b.raymond@cern.ch>
|
||||
7PH <b.raymond@protonmail.com>
|
||||
Adrian Labbé <adrilabbelol@gmail.com>
|
||||
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||
Aleksandr Andriienko <oandriie@redhat.com>
|
||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||
Alexander Olsson <noseglid@gmail.com>
|
||||
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
|
||||
Alexey Kontsevoy <alexey@gravitational.com>
|
||||
Andres Mejia <amejia004@gmail.com>
|
||||
Anish Athalye <me@anishathalye.com>
|
||||
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>
|
||||
Artem Arbatskiy <artyom.arbatskiy@gmail.com>
|
||||
Austin Robertson <austinrobertson@gmail.com>
|
||||
ayapi <colors.aya@gmail.com>
|
||||
Ben Hall <ben@benhall.me.uk>
|
||||
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
||||
Benjamin Raymond <b.raymond@cern.ch>
|
||||
Benjamin Woodruff <github@benjam.info>
|
||||
Bill Church <billchurch@users.noreply.github.com>
|
||||
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||
Brandon Bayer <b@bayer.ws>
|
||||
Brian Mock <brian@mockbrian.com>
|
||||
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
|
||||
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
|
||||
Carson Anderson <carson@betterservers.com>
|
||||
CHaBou <chabup@delean.fr>
|
||||
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>
|
||||
Dan Kaplun <dbkaplun@twitch.tv>
|
||||
Darin Morrison <freebroccolo@users.noreply.github.com>
|
||||
dcylabs <dcylabs@gmail.com>
|
||||
Dominik Csapak <d.csapak@proxmox.com>
|
||||
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>
|
||||
Ian Lewis <ianlewis@google.com>
|
||||
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>
|
||||
Jeff Principe <princjef@gmail.com>
|
||||
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 Kohler <jon@nutanix.com>
|
||||
Jon Masters <jon.masters@sky.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
Jose Anton <roke22@gmail.com>
|
||||
jpoth <poth.john@gmail.com>
|
||||
Justin Luk <jluk@users.noreply.github.com>
|
||||
Justin Mecham <justin@mecham.me>
|
||||
Kirill Merkushev <lanwen@yandex.ru>
|
||||
Krasimir Tsonev <krasimir@outset.ws>
|
||||
Ledion Bitincka <lbitincka@gmail.com>
|
||||
Linus Unnebäck <linus@folkdatorn.se>
|
||||
Luca <LucaT1@users.noreply.github.com>
|
||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||
Lukas Drgon <lukas.drgon@gmail.com>
|
||||
Lukas Geiger <lukas.geiger94@gmail.com>
|
||||
Maël Nison <nison.mael@gmail.com>
|
||||
Marc Dumais <marc.dumais@ericsson.com>
|
||||
Marek Libra <mlibra@redhat.com>
|
||||
Markus F.X.J. Oberhumer <markus@oberhumer.com>
|
||||
Martin Chloride <i@martincl2.me>
|
||||
Martin Koppehel <martin.koppehel@st.ovgu.de>
|
||||
Martin Wang <jiahaow@ca.ibm.com>
|
||||
Matt Bierner <matb@microsoft.com>
|
||||
Matthew James <matthew.d.james87@gmail.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||
mofux <t.zilz@mofux.org>
|
||||
muji <muji@tmpfs.org>
|
||||
Nicolas Ramz <nicolas.ramz@gmail.com>
|
||||
npezza93 <npezza93@gmail.com>
|
||||
Oleksandr Andriienko <oandriie@redhat.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
Peng Xiao <pengxiao@outlook.com>
|
||||
Peter Baumgarten <me@peterbaumgarten.com>
|
||||
Philip Olson <philip.olson@protonmail.ch>
|
||||
PowerHat <31401273+7PH@users.noreply.github.com>
|
||||
PowerHat <b.raymond@protonmail.com>
|
||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
||||
pro-src <rodneyd.teal@gmail.com>
|
||||
Rick Baker <rick@ricktbaker.com>
|
||||
runarberg <runar@greenqloud.com>
|
||||
Saad Malik <simfox3@gmail.com>
|
||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
||||
Saswat Das <saswatds@users.noreply.github.com>
|
||||
Saul Costa <saul@codevolve.com>
|
||||
Segev Finer <segev208@gmail.com>
|
||||
Shuanglei Tao <tsl0922@gmail.com>
|
||||
sitzmar <sitzmar@gmail.com>
|
||||
Steven Silvester <steven.silvester@ieee.org>
|
||||
stuicey <stuicey@users.noreply.github.com>
|
||||
Sven-Hendrik Haase <svenstaro@gmail.com>
|
||||
t-amqi <umberos@gmail.com>
|
||||
Thanasis Daglis <thanasis@sourcelair.com>
|
||||
thomas <thomas@kreativkonzentrat.de>
|
||||
Thomas Zilz <t.zilz@mofux.org>
|
||||
Tine Jozelj <tine.jozelj@outlook.com>
|
||||
Tyler Jewell <tjewell@codenvy.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
Viraj Sinha <virajosinha@gmail.com>
|
||||
yutaka <shigure@refy.net>
|
||||
YuviPanda <yuvipanda@gmail.com>
|
||||
ZHAO Xudong <zxdong@gmail.com>
|
||||
赵雪珂 <zhaoxueke@onething.net>
|
||||
+6
-6
@@ -33,21 +33,21 @@ opening an issue, read these pointers.
|
||||
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
|
||||
|
||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
|
||||
- Make your changes
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running.
|
||||
- Make your changes.
|
||||
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
||||
- Follow the general code style of the rest of the project (see below).
|
||||
- Submit a pull request
|
||||
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
||||
Don't put more than one feature/fix in a single pull request.
|
||||
|
||||
By contributing code to xterm.js you
|
||||
By contributing code to xterm.js you:
|
||||
|
||||
- agree to license the contributed code under xterm.js' [MIT
|
||||
- Agree to license the contributed code under xterm.js' [MIT
|
||||
license](LICENSE).
|
||||
|
||||
- confirm that you have the right to contribute and license the code
|
||||
- Confirm that you have the right to contribute and license the code
|
||||
in question. (Either you hold all rights on the code, or the rights
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2017-2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
|
||||
@@ -2,25 +2,22 @@
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||
|
||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||
|
||||
It enables applications to provide fully featured terminals to their users and create great development experiences.
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
|
||||
## Features
|
||||
- **Text-based application support**: Use xterm.js to work with applications like `bash`, `git` etc.
|
||||
- **Curses-based application support**: Use xterm.js to work with applications like `vim`, `tmux` etc.
|
||||
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
|
||||
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
|
||||
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard
|
||||
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
|
||||
- **Modular, event-based API**: Lets you build addons and themes with ease
|
||||
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim` and `tmux`, this includes support for curses-based apps and mouse event support.
|
||||
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
|
||||
## What xterm.js is not
|
||||
- Xterm.js is not a terminal application that you can download and use on your computer
|
||||
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output)
|
||||
|
||||
- Xterm.js is not a terminal application that you can download and use on your computer.
|
||||
- Xterm.js is not `bash`. Xterm.js can be connected to processes like `bash` and let you interact with them (provide input, receive output).
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -30,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https://
|
||||
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.
|
||||
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. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
@@ -44,39 +41,30 @@ 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 \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
### Importing
|
||||
|
||||
The proposed way to load xterm.js is via the ES6 module syntax.
|
||||
The recommended way to load xterm.js is via the ES6 module syntax:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
### API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
### 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.
|
||||
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 the [public API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts).
|
||||
|
||||
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from xterm;
|
||||
import { Terminal } from 'xterm';
|
||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
|
||||
var xterm = new Terminal(); // Instantiate the terminal
|
||||
@@ -87,25 +75,16 @@ You will also need to include the addon's CSS file if it has one in the folder.
|
||||
|
||||
#### 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.:
|
||||
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. `(xterm as any).fit()`. Alternatively, you can import the addon function and enhance the terminal on demand. This has better typing support and is friendly to treeshaking.
|
||||
|
||||
```typescript
|
||||
import { Terminal } from 'xterm';
|
||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||
const xterm = new Terminal();
|
||||
|
||||
// Fit the terminal when necessary:
|
||||
fit(xterm);
|
||||
fit(xterm); // Fit the terminal when necessary
|
||||
```
|
||||
|
||||
#### 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:
|
||||
@@ -116,26 +95,28 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
- Safari latest
|
||||
- IE11
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
|
||||
|
||||
## API
|
||||
|
||||
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js
|
||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js
|
||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
|
||||
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
||||
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
||||
- [**Codenvy**](http://www.codenvy.com): Cloud workspaces for development teams.
|
||||
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by `xterm.js`.
|
||||
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using `xterm.js`, socket.io, and ssh2.
|
||||
- [**CoderPad**](https://coderpad.io): Online interviewing platform for programmers. Run code in many programming languages, with results displayed by xterm.js.
|
||||
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
|
||||
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
||||
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
||||
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
|
||||
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
|
||||
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||
@@ -143,9 +124,8 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Hexlet**](https://en.hexlet.io): Practical programming courses (JavaScript, PHP, Unix, databases, functional programming). A steady path from the first line of code to the first job.
|
||||
- [**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
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**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 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.
|
||||
- [**Proxmox VE**](https://www.proxmox.com/en/proxmox-ve): Proxmox VE is a complete open-source platform for enterprise virtualization. It uses xterm.js for container terminals and the host shell.
|
||||
@@ -156,23 +136,28 @@ computational environment for Jupyter, supporting interactive data science and s
|
||||
- [**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
|
||||
- [**rtty**](https://github.com/zhaojh329/rtty): Access your terminals from anywhere via the web.
|
||||
- [**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.
|
||||
- [**Azure Data Studio**](https://github.com/Microsoft/azuredatastudio): 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
|
||||
- [**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
|
||||
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
|
||||
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
|
||||
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
|
||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
|
||||
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
||||
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
|
||||
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
|
||||
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
|
||||
- [**info-beamer hosted**](https://info-beamer.com): Uses Xterm.js to manage digital signage devices from the web dashboard.
|
||||
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
|
||||
- [**Jumpserver**](https://github.com/jumpserver/luna): Jumpserver Luna project, Jumpserver is a bastion server project, Luna use xterm.js for web terminal emulation.
|
||||
- [**LxdMosaic**](https://github.com/turtle0x1/LxdMosaic): Uses xterm.js to give terminal access to containers through LXD
|
||||
- [**CodeInterview.io**](https://codeinterview.io): A coding interview platform in 25+ languages and many web frameworks. Uses xterm.js to provide shell access.
|
||||
- [**Bastillion**](https://www.bastillion.io): Bastillion is an open-source web-based SSH console that centrally manages administrative access to systems.
|
||||
- [**PHP App Server**](https://github.com/cubiclesoft/php-app-server/): Create lightweight, installable almost-native applications for desktop OSes. ExecTerminal (nicely wraps the xterm.js Terminal), TerminalManager, and RunProcessSDK are self-contained, reusable ES5+ compliant Javascript components.
|
||||
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
@@ -182,7 +167,17 @@ Do you use xterm.js in your application as well? Please [open a Pull Request](ht
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
|
||||
|
||||
```
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -192,6 +187,6 @@ You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Co
|
||||
|
||||
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)<br>
|
||||
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
+51
-4
@@ -21,7 +21,7 @@ jobs:
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
@@ -39,13 +39,13 @@ jobs:
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
- script: |
|
||||
yarn test-coverage
|
||||
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
|
||||
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCHNAME
|
||||
yarn coveralls
|
||||
displayName: 'Generate and publish coverage'
|
||||
|
||||
@@ -62,7 +62,54 @@ jobs:
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
displayName: 'Unit tests'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
|
||||
- job: IntegrationTests
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: "1.9.4"
|
||||
displayName: 'Install Yarn'
|
||||
- script: |
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn start &
|
||||
sleep 10
|
||||
yarn test-api --headless
|
||||
displayName: 'Integration tests'
|
||||
|
||||
- job: Release
|
||||
dependsOn:
|
||||
- Linux
|
||||
- macOS
|
||||
- Windows
|
||||
- IntegrationTests
|
||||
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['Build.SourceBranch'], 'refs/heads/release/*')))
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: "1.9.4"
|
||||
displayName: 'Install Yarn'
|
||||
- script: |
|
||||
yarn
|
||||
BUILD_DIR=dist npm run build
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
|
||||
displayName: 'Publish to npm'
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
#! /usr/bin/env sh
|
||||
|
||||
tail --lines=+3 AUTHORS > AUTHORS.tmp
|
||||
git log --format='%aN <%aE>' >> AUTHORS.tmp
|
||||
echo "List of xterm.js contributors. Updated before every release.\n" > AUTHORS
|
||||
sort -u AUTHORS.tmp >> AUTHORS
|
||||
rm -f AUTHORS.tmp
|
||||
@@ -1,27 +0,0 @@
|
||||
#! /usr/bin/env sh
|
||||
|
||||
# Usage: ./bin/prepare-release x.y.z
|
||||
# x.y.z should be semver (e.g. 1.0.0)
|
||||
|
||||
set -e
|
||||
|
||||
NEW_VERSION=$1
|
||||
CURRENT_PACKAGE_JSON_VERSION=$(cat package.json \
|
||||
| grep version \
|
||||
| head -1 \
|
||||
| awk -F: '{ print $2 }' \
|
||||
| sed 's/[",]//g' \
|
||||
| tr -d '[[:space:]]')
|
||||
|
||||
# Build xterm.js into `dist`
|
||||
export BUILD_DIR=dist
|
||||
npm run build
|
||||
|
||||
# Update AUTHORS file
|
||||
sh bin/generate-authors
|
||||
|
||||
# Update version in package.json
|
||||
sed -i "s/\"version\": \"$CURRENT_PACKAGE_JSON_VERSION\"/\"version\": \"$NEW_VERSION\"/g" package.json
|
||||
|
||||
git commit -S -s -a -m "Bump version to $NEW_VERSION"
|
||||
git tag $NEW_VERSION
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const packageJson = require('../package.json');
|
||||
|
||||
// Setup auth
|
||||
fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
|
||||
|
||||
// Determine if this is a stable or beta release
|
||||
const publishedVersions = getPublishedVersions();
|
||||
const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
|
||||
|
||||
// Get the next version
|
||||
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion();
|
||||
console.log(`Publishing version: ${nextVersion}`);
|
||||
|
||||
// Set the version in package.json
|
||||
const packageJsonFile = path.resolve(__dirname, '..', 'package.json');
|
||||
packageJson.version = nextVersion;
|
||||
fs.writeFileSync(packageJsonFile, JSON.stringify(packageJson, null, 2));
|
||||
|
||||
// Publish
|
||||
const args = ['publish'];
|
||||
if (!isStableRelease) {
|
||||
args.push('--tag', 'beta');
|
||||
}
|
||||
const result = cp.spawn('npm', args, { stdio: 'inherit' });
|
||||
result.on('exit', code => process.exit(code));
|
||||
|
||||
function getNextBetaVersion() {
|
||||
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
|
||||
console.error('The package.json version must be of the form x.y.z');
|
||||
process.exit(1);
|
||||
}
|
||||
const tag = 'beta';
|
||||
const stableVersion = packageJson.version.split('.');
|
||||
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
|
||||
const publishedVersions = getPublishedVersions(nextStableVersion, tag);
|
||||
if (publishedVersions.length === 0) {
|
||||
return `${nextStableVersion}-${tag}1`;
|
||||
}
|
||||
const latestPublishedVersion = publishedVersions.sort((a, b) => {
|
||||
const aVersion = parseInt(a.substr(a.search(/[0-9]+$/)));
|
||||
const bVersion = parseInt(b.substr(b.search(/[0-9]+$/)));
|
||||
return aVersion > bVersion ? -1 : 1;
|
||||
})[0];
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
|
||||
return `${nextStableVersion}-${tag}${latestTagVersion + 1}`;
|
||||
}
|
||||
|
||||
function getPublishedVersions(version, tag) {
|
||||
const versionsProcess = cp.spawnSync('npm', ['view', packageJson.name, 'versions', '--json']);
|
||||
const versionsJson = JSON.parse(versionsProcess.stdout);
|
||||
if (tag) {
|
||||
return versionsJson.filter(v => !v.search(new RegExp(`${version}-${tag}[0-9]+`)));
|
||||
}
|
||||
return versionsJson;
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
#! /usr/bin/env sh
|
||||
|
||||
# Usage: ./bin/release x.y.z
|
||||
# x.y.z should be semver (e.g. 1.0.0)
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "$1" ];
|
||||
then
|
||||
echo "No version supplied. Please a version argument\n"
|
||||
echo "Usage: $0 VERSION\n"
|
||||
echo "Example: $0 1.0.0"
|
||||
exit
|
||||
fi
|
||||
|
||||
NEW_VERSION=$1
|
||||
|
||||
./bin/prepare-release $NEW_VERSION
|
||||
|
||||
git push && \
|
||||
git push --tags && \
|
||||
npm publish
|
||||
+63
-53
@@ -8,31 +8,26 @@
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
import * as attach from '../lib/addons/attach/attach';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
|
||||
import * as fit from '../lib/addons/fit/fit';
|
||||
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||
import * as search from '../lib/addons/search/search';
|
||||
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
import { Terminal as TerminalType } from 'xterm';
|
||||
import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: TerminalType;
|
||||
Terminal?: typeof TerminalType;
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
let term;
|
||||
let searchAddon: SearchAddon;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
@@ -50,7 +45,13 @@ function setPadding(): void {
|
||||
term.fit();
|
||||
}
|
||||
|
||||
createTerminal();
|
||||
function getSearchOptions(): ISearchOptions {
|
||||
return {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
}
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
// If the terminal exists dispose of it, otherwise recreate it
|
||||
@@ -67,16 +68,32 @@ const disposeRecreateButtonHandler = () => {
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
if (document.location.pathname === '/test') {
|
||||
window.Terminal = Terminal;
|
||||
} else {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
}
|
||||
|
||||
function createTerminal(): void {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({});
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
windowsMode: isWindows
|
||||
} as ITerminalOptions);
|
||||
|
||||
// Load addons
|
||||
const typedTerm = term as TerminalType;
|
||||
typedTerm.loadAddon(new WebLinksAddon());
|
||||
searchAddon = new SearchAddon();
|
||||
typedTerm.loadAddon(searchAddon);
|
||||
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.on('resize', (size: { cols: number, rows: number }) => {
|
||||
term.onResize((size: { cols: number, rows: number }) => {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
@@ -90,33 +107,20 @@ function createTerminal(): void {
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
}
|
||||
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||
const searchOptions = getSearchOptions();
|
||||
searchOptions.incremental = e.key !== `Enter`;
|
||||
searchAddon.findNext(actionElements.findNext.value, searchOptions);
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findPrevious(actionElements.findPrevious.value, searchOptions);
|
||||
|
||||
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||
if (e.key === `Enter`) {
|
||||
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -145,7 +149,14 @@ function createTerminal(): void {
|
||||
}
|
||||
|
||||
function runRealTerminal(): void {
|
||||
term.attach(socket);
|
||||
/**
|
||||
* The demo defaults to string transport by default.
|
||||
* To run it with UTF8 binary transport, swap comment on
|
||||
* the lines below. (Must also be switched in server.js)
|
||||
*/
|
||||
term.loadAddon(new AttachAddon(socket));
|
||||
// term.loadAddon(new AttachAddon(socket, {inputUtf8: true}));
|
||||
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
@@ -166,24 +177,21 @@ function runFakeTerminal(): void {
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term._core.register(term.addDisposableListener('key', (key, ev) => {
|
||||
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
|
||||
term.onKey((e: { key: string, domEvent: KeyboardEvent }) => {
|
||||
const ev = e.domEvent;
|
||||
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey;
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Do not delete the prompt
|
||||
if (term.x > 2) {
|
||||
if (term._core.buffer.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
term.write(e.key);
|
||||
}
|
||||
}));
|
||||
|
||||
term._core.register(term.addDisposableListener('paste', (data, ev) => {
|
||||
term.write(data);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function initOptions(term: TerminalType): void {
|
||||
@@ -207,8 +215,7 @@ function initOptions(term: TerminalType): void {
|
||||
fontFamily: null,
|
||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
rendererType: ['dom', 'canvas'],
|
||||
experimentalBufferLineImpl: ['JsArray', 'TypedArray']
|
||||
rendererType: ['dom', 'canvas']
|
||||
};
|
||||
const options = Object.keys((<any>term)._core.options);
|
||||
const booleanOptions = [];
|
||||
@@ -235,7 +242,7 @@ function initOptions(term: TerminalType): void {
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
@@ -264,8 +271,11 @@ function initOptions(term: TerminalType): void {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'cols' || o === 'rows') {
|
||||
updateTerminalSize();
|
||||
} else if (o === 'lineHeight') {
|
||||
term.setOption(o, parseFloat(input.value));
|
||||
updateTerminalSize();
|
||||
} else {
|
||||
term.setOption(o, parseInt(input.value, 10));
|
||||
term.setOption(o, parseInt(input.value));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -286,8 +296,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
|
||||
function updateTerminalSize(): void {
|
||||
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
|
||||
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
|
||||
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||
const width = (cols * term._core._renderCoordinator.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core._renderCoordinator.dimensions.actualCellHeight).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.fit();
|
||||
|
||||
+3
-3
@@ -2,8 +2,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
@@ -16,7 +16,7 @@
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||
</p>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
+120
-82
@@ -1,100 +1,138 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var expressWs = require('express-ws')(app);
|
||||
var expressWs = require('express-ws');
|
||||
var os = require('os');
|
||||
var pty = require('node-pty');
|
||||
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
/**
|
||||
* Whether to use UTF8 binary transport.
|
||||
* (Must also be switched in client.ts)
|
||||
*/
|
||||
const USE_BINARY_UTF8 = false;
|
||||
|
||||
app.use('/build', express.static(__dirname + '/../build'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
function startServer() {
|
||||
var app = express();
|
||||
expressWs(app);
|
||||
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.get('/dist/client-bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||
});
|
||||
app.use('/src', express.static(__dirname + '/../src'));
|
||||
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
|
||||
|
||||
app.post('/terminals', function (req, res) {
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd: process.env.PWD,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.post('/terminals/:pid/size', function (req, res) {
|
||||
var pid = parseInt(req.params.pid),
|
||||
cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = terminals[pid];
|
||||
app.get('/test', function(req, res){
|
||||
res.sendFile(__dirname + '/test.html');
|
||||
});
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
res.end();
|
||||
});
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
app.get('/dist/client-bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||
});
|
||||
|
||||
function buffer(socket, timeout) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
s += data;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(s);
|
||||
s = '';
|
||||
sender = null;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = buffer(ws, 5);
|
||||
app.post('/terminals', function (req, res) {
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd: process.env.PWD,
|
||||
env: process.env,
|
||||
encoding: USE_BINARY_UTF8 ? null : 'utf8'
|
||||
});
|
||||
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
send(data);
|
||||
} catch (ex) {
|
||||
// The WebSocket is not open, ignore
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.post('/terminals/:pid/size', function (req, res) {
|
||||
var pid = parseInt(req.params.pid),
|
||||
cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = terminals[pid];
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
|
||||
// string message buffering
|
||||
function buffer(socket, timeout) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
s += data;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(s);
|
||||
s = '';
|
||||
sender = null;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
// binary message buffering
|
||||
function bufferUtf8(socket, timeout) {
|
||||
let buffer = [];
|
||||
let sender = null;
|
||||
let length = 0;
|
||||
return (data) => {
|
||||
buffer.push(data);
|
||||
length += data.length;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(buffer, length));
|
||||
buffer = [];
|
||||
sender = null;
|
||||
length = 0;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||
|
||||
var port = process.env.PORT || 3000,
|
||||
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
send(data);
|
||||
} catch (ex) {
|
||||
// The WebSocket is not open, ignore
|
||||
}
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
|
||||
console.log('App listening to http://' + host + ':' + port);
|
||||
app.listen(port, host);
|
||||
var port = process.env.PORT || 3000,
|
||||
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
|
||||
|
||||
console.log('App listening to http://127.0.0.1:' + port);
|
||||
app.listen(port, host);
|
||||
}
|
||||
|
||||
module.exports = startServer;
|
||||
|
||||
+9
-3
@@ -5,12 +5,11 @@
|
||||
* This file is the entry point for browserify.
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
const startServer = require('./server.js');
|
||||
|
||||
// Launch server
|
||||
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
|
||||
startServer();
|
||||
|
||||
// Build/watch client source
|
||||
const clientConfig = {
|
||||
@@ -22,10 +21,17 @@ const clientConfig = {
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, '..'), 'node_modules'],
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js integration test fixture</title>
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="terminal-container"></div>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,87 +0,0 @@
|
||||
var express = require('express');
|
||||
var app = express();
|
||||
var expressWs = require('express-ws')(app);
|
||||
var os = require('os');
|
||||
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.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '../style.css');
|
||||
});
|
||||
|
||||
app.get('/main.js', function(req, res){
|
||||
res.sendFile(__dirname + '/main.js');
|
||||
});
|
||||
|
||||
app.post('/terminals', function (req, res) {
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
encoding: null,
|
||||
name: 'xterm-color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
cwd: process.env.PWD,
|
||||
env: process.env
|
||||
});
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.post('/terminals/:pid/size', function (req, res) {
|
||||
var pid = parseInt(req.params.pid),
|
||||
cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = terminals[pid];
|
||||
|
||||
term.resize(cols, rows);
|
||||
console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.');
|
||||
res.end();
|
||||
});
|
||||
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
|
||||
term.on('data', function(data) {
|
||||
try {
|
||||
ws.send(data);
|
||||
} catch (ex) {
|
||||
// The WebSocket is not open, ignore
|
||||
}
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
|
||||
var port = process.env.PORT || 3000,
|
||||
host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0';
|
||||
|
||||
console.log('App listening to http://' + host + ':' + port);
|
||||
app.listen(port, host);
|
||||
@@ -1,128 +0,0 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="/demo/style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
<script src="/build/xterm.js" ></script>
|
||||
|
||||
<script src="/build/addons/attach/attach.js" ></script>
|
||||
<script src="/zmodemjs/zmodem.js"></script>
|
||||
<script src="/build/addons/zmodem/zmodem.js" ></script>
|
||||
|
||||
<script src="/build/addons/fit/fit.js" ></script>
|
||||
<script src="/build/addons/fullscreen/fullscreen.js" ></script>
|
||||
<script src="/build/addons/search/search.js" ></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>xterm.js: xterm, in the browser</h1>
|
||||
|
||||
<div id="terminal-container"></div>
|
||||
|
||||
<div id="zmodem_controls">
|
||||
<form id="zm_start" style="display: none" action="javascript:void(0)">
|
||||
ZMODEM detected: Start ZMODEM session?
|
||||
<label><input id="zmstart_yes" name="zmstart" type=radio checked value="1"> Yes</label>
|
||||
|
||||
<label><input name="zmstart" type=radio value=""> No</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<form id="zm_offer" style="display: none" action="javascript:void(0)">
|
||||
<p>ZMODEM File offered!</p>
|
||||
|
||||
<label><input id="zmaccept_yes" name="zmaccept" type=radio checked value="1"> Accept</label>
|
||||
|
||||
<label><input name="zmaccept" type=radio value=""> Skip</label>
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
|
||||
<div id="zm_file" style="display: none">
|
||||
<div>Name: <span id="name"></span></div>
|
||||
<div>Size: <span id="size"></span></div>
|
||||
<div>Last modified: <span id="mtime"></span></div>
|
||||
<div>Mode: <span id="mode"></span></div>
|
||||
<br>
|
||||
<div>Conversion: <span id="zfile_conversion"></span></div>
|
||||
<div>Management: <span id="zfile_management"></span></div>
|
||||
<div>Transport: <span id="zfile_transport"></span></div>
|
||||
<div>Sparse? <span id="zfile_sparse"></span></div>
|
||||
<br>
|
||||
<div>Files remaining in batch: <span id="files_remaining"></span></div>
|
||||
<div>Bytes remaining in batch: <span id="bytes_remaining"></span></div>
|
||||
</div>
|
||||
|
||||
<form id="zm_progress" style="display: none" action="javascript:void(0)">
|
||||
<div><span id="percent_received"></span>% (<span id="bytes_received"></span> bytes) received</div>
|
||||
<button id="zm_progress_skipper" type="button" onclick="skip_current_file();">Skip File</button>
|
||||
</form>
|
||||
|
||||
<form id="zm_choose" style="display: none" action="javascript:void(0)">
|
||||
<label>Choose file(s): <input id="zm_files" type="file" multiple></label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" checked id="zmodem-auto"> Accept all ZMODEM prompts<sup>*</sup></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
cursorStyle
|
||||
<select id="option-cursor-style">
|
||||
<option value="block">block</option>
|
||||
<option value="underline">underline</option>
|
||||
<option value="bar">bar</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
bellStyle
|
||||
<select id="option-bell-style">
|
||||
<option value="">none</option>
|
||||
<option value="sound">sound</option>
|
||||
<option value="visual">visual</option>
|
||||
<option value="both">both</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
|
||||
</p>
|
||||
<div>
|
||||
<h3>Size</h3>
|
||||
<div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="cols">Columns</label>
|
||||
<input type="number" id="cols" />
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="rows">Rows</label>
|
||||
<input type="number" id="rows" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for xterm.js evaluation purposes only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||
<p><sup>*</sup> ZMODEM file transfers are supported via an addon. To try it out, install <a href="https://ohse.de/uwe/software/lrzsz.html"><code>lrzsz</code></a> onto the remote peer, then run <code>rz</code> to send from your browser or <code>sz <file></code> to send from the remote peer.</p>
|
||||
<script src="main.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,388 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
var term,
|
||||
protocol,
|
||||
socketURL,
|
||||
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'),
|
||||
findPrevious: document.querySelector('#find-previous')
|
||||
},
|
||||
optionElements = {
|
||||
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||
scrollback: document.querySelector('#option-scrollback'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth'),
|
||||
bellStyle: document.querySelector('#option-bell-style')
|
||||
},
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows');
|
||||
|
||||
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.charMeasure.width + 20 /*room for scrollbar*/).toString() + 'px';
|
||||
var height = (rows * term.charMeasure.height).toString() + 'px';
|
||||
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
|
||||
colsElement.addEventListener('change', setTerminalSize);
|
||||
rowsElement.addEventListener('change', setTerminalSize);
|
||||
|
||||
actionElements.findNext.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findNext(actionElements.findNext.value);
|
||||
}
|
||||
});
|
||||
actionElements.findPrevious.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findPrevious(actionElements.findPrevious.value);
|
||||
}
|
||||
});
|
||||
|
||||
optionElements.cursorBlink.addEventListener('change', function () {
|
||||
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
|
||||
});
|
||||
optionElements.cursorStyle.addEventListener('change', function () {
|
||||
term.setOption('cursorStyle', optionElements.cursorStyle.value);
|
||||
});
|
||||
optionElements.bellStyle.addEventListener('change', function () {
|
||||
term.setOption('bellStyle', optionElements.bellStyle.value);
|
||||
});
|
||||
optionElements.scrollback.addEventListener('change', function () {
|
||||
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
|
||||
});
|
||||
optionElements.tabstopwidth.addEventListener('change', function () {
|
||||
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
|
||||
});
|
||||
|
||||
createTerminal();
|
||||
|
||||
function createTerminal() {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({
|
||||
cursorBlink: optionElements.cursorBlink.checked,
|
||||
scrollback: parseInt(optionElements.scrollback.value, 10),
|
||||
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
|
||||
});
|
||||
term.on('resize', function (size) {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
var cols = size.cols,
|
||||
rows = size.rows,
|
||||
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.fit();
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
res.text().then(function (pid) {
|
||||
window.pid = pid;
|
||||
socketURL += pid;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
|
||||
term.zmodemAttach(socket, {
|
||||
noTerminalWriteOutsideSession: true,
|
||||
} );
|
||||
|
||||
term.on("zmodemRetract", () => {
|
||||
start_form.style.display = "none";
|
||||
start_form.onsubmit = null;
|
||||
});
|
||||
|
||||
term.on("zmodemDetect", (detection) => {
|
||||
function do_zmodem() {
|
||||
term.detach();
|
||||
let zsession = detection.confirm();
|
||||
|
||||
var promise;
|
||||
|
||||
if (zsession.type === "receive") {
|
||||
promise = _handle_receive_session(zsession);
|
||||
}
|
||||
else {
|
||||
promise = _handle_send_session(zsession);
|
||||
}
|
||||
|
||||
promise.catch( console.error.bind(console) ).then( () => {
|
||||
term.attach(socket);
|
||||
} );
|
||||
}
|
||||
|
||||
if (_auto_zmodem()) {
|
||||
do_zmodem();
|
||||
}
|
||||
else {
|
||||
start_form.style.display = "";
|
||||
start_form.onsubmit = function(e) {
|
||||
start_form.style.display = "none";
|
||||
|
||||
if (document.getElementById("zmstart_yes").checked) {
|
||||
do_zmodem();
|
||||
}
|
||||
else {
|
||||
detection.deny();
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
//----------------------------------------------------------------------
|
||||
// UI STUFF
|
||||
|
||||
function _show_file_info(xfer) {
|
||||
var file_info = xfer.get_details();
|
||||
|
||||
document.getElementById("name").textContent = file_info.name;
|
||||
document.getElementById("size").textContent = file_info.size;
|
||||
document.getElementById("mtime").textContent = file_info.mtime;
|
||||
document.getElementById("files_remaining").textContent = file_info.files_remaining;
|
||||
document.getElementById("bytes_remaining").textContent = file_info.bytes_remaining;
|
||||
|
||||
document.getElementById("mode").textContent = "0" + file_info.mode.toString(8);
|
||||
|
||||
var xfer_opts = xfer.get_options();
|
||||
["conversion", "management", "transport", "sparse"].forEach( (lbl) => {
|
||||
document.getElementById(`zfile_${lbl}`).textContent = xfer_opts[lbl];
|
||||
} );
|
||||
|
||||
document.getElementById("zm_file").style.display = "";
|
||||
}
|
||||
function _hide_file_info() {
|
||||
document.getElementById("zm_file").style.display = "none";
|
||||
}
|
||||
|
||||
function _save_to_disk(xfer, buffer) {
|
||||
return Zmodem.Browser.save_to_disk(buffer, xfer.get_details().name);
|
||||
}
|
||||
|
||||
var skipper_button = document.getElementById("zm_progress_skipper");
|
||||
var skipper_button_orig_text = skipper_button.textContent;
|
||||
|
||||
function _show_progress() {
|
||||
skipper_button.disabled = false;
|
||||
skipper_button.textContent = skipper_button_orig_text;
|
||||
|
||||
document.getElementById("bytes_received").textContent = 0;
|
||||
document.getElementById("percent_received").textContent = 0;
|
||||
|
||||
document.getElementById("zm_progress").style.display = "";
|
||||
}
|
||||
|
||||
function _update_progress(xfer) {
|
||||
var total_in = xfer.get_offset();
|
||||
|
||||
document.getElementById("bytes_received").textContent = total_in;
|
||||
|
||||
var percent_received = 100 * total_in / xfer.get_details().size;
|
||||
document.getElementById("percent_received").textContent = percent_received.toFixed(2);
|
||||
}
|
||||
|
||||
function _hide_progress() {
|
||||
document.getElementById("zm_progress").style.display = "none";
|
||||
}
|
||||
|
||||
var start_form = document.getElementById("zm_start");
|
||||
|
||||
function _auto_zmodem() {
|
||||
return document.getElementById("zmodem-auto").checked;
|
||||
}
|
||||
|
||||
// END UI STUFF
|
||||
//----------------------------------------------------------------------
|
||||
|
||||
function _handle_receive_session(zsession) {
|
||||
zsession.on("offer", function(xfer) {
|
||||
current_receive_xfer = xfer;
|
||||
|
||||
_show_file_info(xfer);
|
||||
|
||||
var offer_form = document.getElementById("zm_offer");
|
||||
|
||||
function on_form_submit() {
|
||||
offer_form.style.display = "none";
|
||||
|
||||
//START
|
||||
//if (offer_form.zmaccept.value) {
|
||||
if (_auto_zmodem() || document.getElementById("zmaccept_yes").checked) {
|
||||
_show_progress();
|
||||
|
||||
var FILE_BUFFER = [];
|
||||
xfer.on("input", (payload) => {
|
||||
_update_progress(xfer);
|
||||
FILE_BUFFER.push( new Uint8Array(payload) );
|
||||
});
|
||||
xfer.accept().then(
|
||||
() => {
|
||||
_save_to_disk(xfer, FILE_BUFFER);
|
||||
},
|
||||
console.error.bind(console)
|
||||
);
|
||||
}
|
||||
else {
|
||||
xfer.skip();
|
||||
}
|
||||
//END
|
||||
}
|
||||
|
||||
if (_auto_zmodem()) {
|
||||
on_form_submit();
|
||||
}
|
||||
else {
|
||||
offer_form.onsubmit = on_form_submit;
|
||||
offer_form.style.display = "";
|
||||
}
|
||||
} );
|
||||
|
||||
var promise = new Promise( (res) => {
|
||||
zsession.on("session_end", () => {
|
||||
_hide_file_info();
|
||||
_hide_progress();
|
||||
res();
|
||||
} );
|
||||
} );
|
||||
|
||||
zsession.start();
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function _handle_send_session(zsession) {
|
||||
var choose_form = document.getElementById("zm_choose");
|
||||
choose_form.style.display = "";
|
||||
|
||||
var file_el = document.getElementById("zm_files");
|
||||
|
||||
var promise = new Promise( (res) => {
|
||||
file_el.onchange = function(e) {
|
||||
choose_form.style.display = "none";
|
||||
|
||||
var files_obj = file_el.files;
|
||||
|
||||
Zmodem.Browser.send_files(
|
||||
zsession,
|
||||
files_obj,
|
||||
{
|
||||
on_offer_response(obj, xfer) {
|
||||
if (xfer) _show_progress();
|
||||
//console.log("offer", xfer ? "accepted" : "skipped");
|
||||
},
|
||||
on_progress(obj, xfer) {
|
||||
_update_progress(xfer);
|
||||
},
|
||||
on_file_complete(obj) {
|
||||
//console.log("COMPLETE", obj);
|
||||
_hide_progress();
|
||||
},
|
||||
}
|
||||
).then(_hide_progress).then(
|
||||
zsession.close.bind(zsession),
|
||||
console.error.bind(console)
|
||||
).then( () => {
|
||||
_hide_file_info();
|
||||
_hide_progress();
|
||||
res();
|
||||
} );
|
||||
};
|
||||
} );
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
//This is here to allow canceling of an in-progress ZMODEM transfer.
|
||||
var current_receive_xfer;
|
||||
|
||||
//Called from HTML directly.
|
||||
function skip_current_file() {
|
||||
current_receive_xfer.skip();
|
||||
|
||||
skipper_button.disabled = true;
|
||||
skipper_button.textContent = "Waiting for server to acknowledge skip …";
|
||||
}
|
||||
|
||||
function runRealTerminal() {
|
||||
term.attach(socket);
|
||||
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
function runFakeTerminal() {
|
||||
if (term._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
term._initialized = true;
|
||||
|
||||
var shellprompt = '$ ';
|
||||
|
||||
term.prompt = function () {
|
||||
term.write('\r\n' + shellprompt);
|
||||
};
|
||||
|
||||
term.writeln('Welcome to xterm.js');
|
||||
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term.writeln('Type some keys and commands to play around.');
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term.on('key', function (key, ev) {
|
||||
var printable = (
|
||||
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
|
||||
);
|
||||
|
||||
if (ev.keyCode == 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode == 8) {
|
||||
// Do not delete the prompt
|
||||
if (term.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
}
|
||||
});
|
||||
|
||||
term.on('paste', function (data, ev) {
|
||||
term.write(data);
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -7,7 +7,7 @@ services:
|
||||
volumes:
|
||||
- ./:/usr/src/app
|
||||
ports:
|
||||
- ${XTERMJS_PORT:3000}:3000
|
||||
- ${XTERMJS_PORT:-3000}:3000
|
||||
command: ["npm", "start"]
|
||||
|
||||
watch:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
/// <reference path="../../typings/xterm.d.ts" />
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
|
||||
namespace constructor {
|
||||
{
|
||||
@@ -21,7 +21,7 @@ namespace constructor {
|
||||
'disableStdin': false,
|
||||
'rows': 1,
|
||||
'scrollback': 10,
|
||||
'tabStopWidth': 2,
|
||||
'tabStopWidth': 2
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,12 @@ namespace methods_core {
|
||||
const t: Terminal = new Terminal();
|
||||
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
|
||||
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
|
||||
const d1: IDisposable = t.addCsiHandler('x',
|
||||
(params: number[], collect: string): boolean => params[0] === 1);
|
||||
d1.dispose();
|
||||
const d2: IDisposable = t.addOscHandler(199,
|
||||
(data: string): boolean => true);
|
||||
d2.dispose();
|
||||
}
|
||||
namespace options {
|
||||
{
|
||||
@@ -149,6 +155,7 @@ namespace methods_core {
|
||||
const r25: string = t.getOption('fontWeightBold');
|
||||
const r26: boolean = t.getOption('allowTransparency');
|
||||
const r27: boolean = t.getOption('rightClickSelectsWord');
|
||||
const r28: boolean = t.getOption('windowsMode');
|
||||
}
|
||||
{
|
||||
const t: Terminal = new Terminal();
|
||||
@@ -171,6 +178,7 @@ namespace methods_core {
|
||||
t.setOption('useFlowControl', true);
|
||||
t.setOption('allowTransparency', true);
|
||||
t.setOption('visualBell', true);
|
||||
t.setOption('windowsMode', true);
|
||||
t.setOption('colors', ['a', 'b']);
|
||||
t.setOption('letterSpacing', 1);
|
||||
t.setOption('cols', 1);
|
||||
|
||||
+9
-5
@@ -16,9 +16,9 @@ const ts = require('gulp-typescript');
|
||||
const util = require('gulp-util');
|
||||
|
||||
const buildDir = process.env.BUILD_DIR || 'build';
|
||||
const tsProject = ts.createProject('tsconfig.json');
|
||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let outDir = tsProject.config.compilerOptions.outDir;
|
||||
const tsProject = ts.createProject('src/tsconfig.json');
|
||||
let srcDir = './src';
|
||||
let outDir = './lib';
|
||||
|
||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||
|
||||
@@ -39,6 +39,10 @@ gulp.task('css', function() {
|
||||
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
gulp.task('css-build', function() {
|
||||
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(buildDir));
|
||||
});
|
||||
|
||||
gulp.task('watch-css', function() {
|
||||
return gulp.watch(`${srcDir}/**/*.css`, ['css']);
|
||||
});
|
||||
@@ -61,7 +65,7 @@ gulp.task('browserify', function() {
|
||||
};
|
||||
let bundleStream = browserify(browserifyOptions)
|
||||
.bundle()
|
||||
.pipe(source('xterm.js'))
|
||||
.pipe(source(`xterm.js`))
|
||||
.pipe(buffer())
|
||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
||||
.pipe(sourcemaps.write('./'))
|
||||
@@ -136,6 +140,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
})
|
||||
});
|
||||
|
||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
||||
gulp.task('build', ['css', 'css-build', 'sorcery', 'sorcery-addons']);
|
||||
gulp.task('test', ['mocha']);
|
||||
gulp.task('default', ['build']);
|
||||
|
||||
+16
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.9.0",
|
||||
"version": "3.14.1",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
@@ -12,10 +12,11 @@
|
||||
"@types/jsdom": "11.0.1",
|
||||
"@types/mocha": "^2.2.33",
|
||||
"@types/node": "6.0.108",
|
||||
"@types/puppeteer": "^1.12.4",
|
||||
"@types/utf8": "^2.1.6",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
"concurrently": "^3.5.1",
|
||||
"coveralls": "^3.0.1",
|
||||
"express": "4.13.4",
|
||||
"express-ws": "2.0.0-rc.1",
|
||||
@@ -28,43 +29,45 @@
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"gulp-util": "3.0.8",
|
||||
"jsdoc": "3.4.3",
|
||||
"jsdom": "^11.11.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"node-pty": "0.7.6",
|
||||
"nodemon": "1.10.2",
|
||||
"nyc": "^11.8.0",
|
||||
"puppeteer": "^1.15.0",
|
||||
"sorcery": "^0.10.0",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"ts-loader": "^4.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "3.1",
|
||||
"typescript": "3.5",
|
||||
"utf8": "^3.0.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"xterm-addon-attach": "0.1.0-beta8",
|
||||
"xterm-addon-search": "0.1.0-beta4",
|
||||
"xterm-addon-web-links": "0.1.0-beta6",
|
||||
"zmodem.js": "^0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node demo/start",
|
||||
"start-debug": "node --inspect-brk demo/start",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||
"pretest": "npm run layering",
|
||||
"test": "npm run mocha",
|
||||
"posttest": "npm run lint",
|
||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||
"test-suite": "gulp mocha-suite --test",
|
||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||
"test-api": "mocha \"**/*.api.js\"",
|
||||
"mocha": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"tsc": "tsc",
|
||||
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
|
||||
"prebuild": "tsc -b ./src/tsconfig.all.json",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build",
|
||||
"prepare": "npm run prebuild",
|
||||
"prepublishOnly": "npm run build",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
|
||||
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"",
|
||||
"layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\""
|
||||
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
|
||||
}
|
||||
}
|
||||
|
||||
+43
-30
@@ -5,10 +5,12 @@
|
||||
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './core/Platform';
|
||||
import { isMac } from './common/Platform';
|
||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
|
||||
import { IRenderDimensions } from './renderer/Types';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
@@ -25,6 +27,7 @@ export class AccessibilityManager extends Disposable {
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
@@ -40,7 +43,12 @@ export class AccessibilityManager extends Disposable {
|
||||
*/
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
private _charsToAnnounce: string = '';
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
@@ -61,7 +69,7 @@ export class AccessibilityManager extends Disposable {
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
@@ -72,22 +80,21 @@ export class AccessibilityManager extends Disposable {
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
|
||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
|
||||
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
|
||||
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
|
||||
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
|
||||
this.register(this._terminal.onKey(e => this._onKey(e.key)));
|
||||
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
|
||||
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
|
||||
// should be listened to instead? That would mean that the order of events are always
|
||||
// guarenteed
|
||||
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
||||
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this.register(this._screenDprMonitor);
|
||||
this._screenDprMonitor.setListener(() => 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
|
||||
// media query that drives the ScreenDprMonitor isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
@@ -197,10 +204,10 @@ export class AccessibilityManager extends Disposable {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
if (shiftedChar !== char) {
|
||||
this._announceCharacter(char);
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
} else {
|
||||
this._announceCharacter(char);
|
||||
this._charsToAnnounce += char;
|
||||
}
|
||||
|
||||
if (char === '\n') {
|
||||
@@ -239,7 +246,7 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end);
|
||||
this._renderRowsDebouncer.refresh(start, end, this._terminal.rows);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
@@ -249,14 +256,17 @@ export class AccessibilityManager extends Disposable {
|
||||
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);
|
||||
if (element) {
|
||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
this._announceCharacters();
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
||||
if (!this._dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
@@ -267,17 +277,20 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
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 += ' ';
|
||||
} else {
|
||||
this._liveRegion.textContent += char;
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._dimensions.actualCellHeight}px`;
|
||||
}
|
||||
|
||||
private _announceCharacters(): void {
|
||||
if (this._charsToAnnounce.length === 0) {
|
||||
return;
|
||||
}
|
||||
this._liveRegion.textContent += this._charsToAnnounce;
|
||||
this._charsToAnnounce = '';
|
||||
}
|
||||
}
|
||||
|
||||
+894
-43
File diff suppressed because it is too large
Load Diff
+312
-150
@@ -3,24 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, IBufferLineConstructor } from './Types';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine, BufferLineJSArray } from './BufferLine';
|
||||
import { DEFAULT_COLOR } from './renderer/atlas/Types';
|
||||
import { CircularList, IInsertEvent } from './common/CircularList';
|
||||
import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
|
||||
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||
import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './core/buffer/BufferReflow';
|
||||
import { Marker } from './core/buffer/Marker';
|
||||
|
||||
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
export const NULL_CELL_CHAR = ' ';
|
||||
export const NULL_CELL_WIDTH = 1;
|
||||
export const NULL_CELL_CODE = 32;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
* following information is stored (in high-level):
|
||||
@@ -39,9 +30,12 @@ export class Buffer implements IBuffer {
|
||||
public tabs: any;
|
||||
public savedY: number;
|
||||
public savedX: number;
|
||||
public savedCurAttr: number;
|
||||
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
|
||||
public markers: Marker[] = [];
|
||||
private _bufferLineConstructor: IBufferLineConstructor;
|
||||
private _nullCell: ICellData = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
private _whitespaceCell: ICellData = CellData.fromCharData([0, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE]);
|
||||
private _cols: number;
|
||||
private _rows: number;
|
||||
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
@@ -53,48 +47,45 @@ export class Buffer implements IBuffer {
|
||||
private _terminal: ITerminal,
|
||||
private _hasScrollback: boolean
|
||||
) {
|
||||
this._cols = this._terminal.cols;
|
||||
this._rows = this._terminal.rows;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setBufferLineFactory(type: string): void {
|
||||
if (type === 'JsArray') {
|
||||
if (this._bufferLineConstructor !== BufferLineJSArray) {
|
||||
this._bufferLineConstructor = BufferLineJSArray;
|
||||
this._recreateLines();
|
||||
}
|
||||
public getNullCell(attr?: IAttributeData): ICellData {
|
||||
if (attr) {
|
||||
this._nullCell.fg = attr.fg;
|
||||
this._nullCell.bg = attr.bg;
|
||||
} else {
|
||||
if (this._bufferLineConstructor !== BufferLine) {
|
||||
this._bufferLineConstructor = BufferLine;
|
||||
this._recreateLines();
|
||||
}
|
||||
this._nullCell.fg = 0;
|
||||
this._nullCell.bg = 0;
|
||||
}
|
||||
return this._nullCell;
|
||||
}
|
||||
|
||||
private _recreateLines(): void {
|
||||
if (!this.lines) return;
|
||||
for (let i = 0; i < this.lines.length; ++i) {
|
||||
const oldLine = this.lines.get(i);
|
||||
const newLine = new this._bufferLineConstructor(oldLine.length);
|
||||
for (let j = 0; j < oldLine.length; ++j) {
|
||||
newLine.set(j, oldLine.get(j));
|
||||
}
|
||||
this.lines.set(i, newLine);
|
||||
public getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
if (attr) {
|
||||
this._whitespaceCell.fg = attr.fg;
|
||||
this._whitespaceCell.bg = attr.bg;
|
||||
} else {
|
||||
this._whitespaceCell.fg = 0;
|
||||
this._whitespaceCell.bg = 0;
|
||||
}
|
||||
return this._whitespaceCell;
|
||||
}
|
||||
|
||||
public getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
|
||||
const fillCharData: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
return new this._bufferLineConstructor(this._terminal.cols, fillCharData, isWrapped);
|
||||
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped);
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
|
||||
return this._hasScrollback && this.lines.maxLength > this._rows;
|
||||
}
|
||||
|
||||
public get isCursorInViewport(): boolean {
|
||||
const absoluteY = this.ybase + this.y;
|
||||
const relativeY = absoluteY - this.ydisp;
|
||||
return (relativeY >= 0 && relativeY < this._terminal.rows);
|
||||
return (relativeY >= 0 && relativeY < this._rows);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,12 +106,12 @@ export class Buffer implements IBuffer {
|
||||
/**
|
||||
* Fills the buffer's viewport with blank lines.
|
||||
*/
|
||||
public fillViewportRows(fillAttr?: number): void {
|
||||
public fillViewportRows(fillAttr?: IAttributeData): void {
|
||||
if (this.lines.length === 0) {
|
||||
if (fillAttr === undefined) {
|
||||
fillAttr = DEFAULT_ATTR;
|
||||
fillAttr = DEFAULT_ATTR_DATA;
|
||||
}
|
||||
let i = this._terminal.rows;
|
||||
let i = this._rows;
|
||||
while (i--) {
|
||||
this.lines.push(this.getBlankLine(fillAttr));
|
||||
}
|
||||
@@ -131,14 +122,13 @@ export class Buffer implements IBuffer {
|
||||
* Clears the buffer to it's initial state, discarding all previous data.
|
||||
*/
|
||||
public clear(): void {
|
||||
this.setBufferLineFactory(this._terminal.options.experimentalBufferLineImpl);
|
||||
this.ydisp = 0;
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
this.x = 0;
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows));
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this._terminal.rows - 1;
|
||||
this.scrollBottom = this._rows - 1;
|
||||
this.setupTabStops();
|
||||
}
|
||||
|
||||
@@ -148,6 +138,9 @@ export class Buffer implements IBuffer {
|
||||
* @param newRows The new number of rows.
|
||||
*/
|
||||
public resize(newCols: number, newRows: number): void {
|
||||
// store reference to null cell with default attrs
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
|
||||
// Increase max length if needed before adjustments to allow space to fill
|
||||
// as required.
|
||||
const newMaxLength = this._getCorrectBufferLength(newRows);
|
||||
@@ -158,18 +151,17 @@ export class Buffer implements IBuffer {
|
||||
// The following adjustments should only happen if the buffer has been
|
||||
// initialized/filled.
|
||||
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 = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
|
||||
// Deal with columns increasing (reducing needs to happen after reflow)
|
||||
if (this._cols < newCols) {
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
this.lines.get(i).resize(newCols, ch);
|
||||
this.lines.get(i).resize(newCols, nullCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Resize rows in both directions as needed
|
||||
let addToY = 0;
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this._rows < newRows) {
|
||||
for (let y = this._rows; y < newRows; y++) {
|
||||
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,
|
||||
@@ -183,13 +175,12 @@ 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
|
||||
const fillCharData: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
this.lines.push(new this._bufferLineConstructor(newCols, fillCharData));
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
} else { // (this._rows >= newRows)
|
||||
for (let y = this._rows; y > newRows; y--) {
|
||||
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
|
||||
@@ -229,8 +220,238 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
|
||||
this.scrollBottom = newRows - 1;
|
||||
|
||||
if (this._isReflowEnabled) {
|
||||
this._reflow(newCols, newRows);
|
||||
|
||||
// Trim the end of the line off if cols shrunk
|
||||
if (this._cols > newCols) {
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
this.lines.get(i).resize(newCols, nullCell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._cols = newCols;
|
||||
this._rows = newRows;
|
||||
}
|
||||
|
||||
private get _isReflowEnabled(): boolean {
|
||||
return this._hasScrollback && !this._terminal.options.windowsMode;
|
||||
}
|
||||
|
||||
private _reflow(newCols: number, newRows: number): void {
|
||||
if (this._cols === newCols) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Iterate through rows, ignore the last one as it cannot be wrapped
|
||||
if (newCols > this._cols) {
|
||||
this._reflowLarger(newCols, newRows);
|
||||
} else {
|
||||
this._reflowSmaller(newCols, newRows);
|
||||
}
|
||||
}
|
||||
|
||||
private _reflowLarger(newCols: number, newRows: number): void {
|
||||
const toRemove: number[] = reflowLargerGetLinesToRemove(this.lines, this._cols, newCols, this.ybase + this.y, this.getNullCell(DEFAULT_ATTR_DATA));
|
||||
if (toRemove.length > 0) {
|
||||
const newLayoutResult = reflowLargerCreateNewLayout(this.lines, toRemove);
|
||||
reflowLargerApplyNewLayout(this.lines, newLayoutResult.layout);
|
||||
this._reflowLargerAdjustViewport(newCols, newRows, newLayoutResult.countRemoved);
|
||||
}
|
||||
}
|
||||
|
||||
private _reflowLargerAdjustViewport(newCols: number, newRows: number, countRemoved: number): void {
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
// Adjust viewport based on number of items removed
|
||||
let viewportAdjustments = countRemoved;
|
||||
while (viewportAdjustments-- > 0) {
|
||||
if (this.ybase === 0) {
|
||||
if (this.y > 0) {
|
||||
this.y--;
|
||||
}
|
||||
if (this.lines.length < newRows) {
|
||||
// Add an extra row at the bottom of the viewport
|
||||
this.lines.push(new BufferLine(newCols, nullCell));
|
||||
}
|
||||
} else {
|
||||
if (this.ydisp === this.ybase) {
|
||||
this.ydisp--;
|
||||
}
|
||||
this.ybase--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _reflowSmaller(newCols: number, newRows: number): void {
|
||||
const nullCell = this.getNullCell(DEFAULT_ATTR_DATA);
|
||||
// Gather all BufferLines that need to be inserted into the Buffer here so that they can be
|
||||
// batched up and only committed once
|
||||
const toInsert = [];
|
||||
let countToInsert = 0;
|
||||
// Go backwards as many lines may be trimmed and this will avoid considering them
|
||||
for (let y = this.lines.length - 1; y >= 0; y--) {
|
||||
// Check whether this line is a problem
|
||||
let nextLine = this.lines.get(y) as BufferLine;
|
||||
if (!nextLine || !nextLine.isWrapped && nextLine.getTrimmedLength() <= newCols) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Gather wrapped lines and adjust y to be the starting line
|
||||
const wrappedLines: BufferLine[] = [nextLine];
|
||||
while (nextLine.isWrapped && y > 0) {
|
||||
nextLine = this.lines.get(--y) as BufferLine;
|
||||
wrappedLines.unshift(nextLine);
|
||||
}
|
||||
|
||||
// If these lines contain the cursor don't touch them, the program will handle fixing up
|
||||
// wrapped lines with the cursor
|
||||
const absoluteY = this.ybase + this.y;
|
||||
if (absoluteY >= y && absoluteY < y + wrappedLines.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lastLineLength = wrappedLines[wrappedLines.length - 1].getTrimmedLength();
|
||||
const destLineLengths = reflowSmallerGetNewLineLengths(wrappedLines, this._cols, newCols);
|
||||
const linesToAdd = destLineLengths.length - wrappedLines.length;
|
||||
let trimmedLines: number;
|
||||
if (this.ybase === 0 && this.y !== this.lines.length - 1) {
|
||||
// If the top section of the buffer is not yet filled
|
||||
trimmedLines = Math.max(0, this.y - this.lines.maxLength + linesToAdd);
|
||||
} else {
|
||||
trimmedLines = Math.max(0, this.lines.length - this.lines.maxLength + linesToAdd);
|
||||
}
|
||||
|
||||
// Add the new lines
|
||||
const newLines: BufferLine[] = [];
|
||||
for (let i = 0; i < linesToAdd; i++) {
|
||||
const newLine = this.getBlankLine(DEFAULT_ATTR_DATA, true) as BufferLine;
|
||||
newLines.push(newLine);
|
||||
}
|
||||
if (newLines.length > 0) {
|
||||
toInsert.push({
|
||||
// countToInsert here gets the actual index, taking into account other inserted items.
|
||||
// using this we can iterate through the list forwards
|
||||
start: y + wrappedLines.length + countToInsert,
|
||||
newLines
|
||||
});
|
||||
countToInsert += newLines.length;
|
||||
}
|
||||
wrappedLines.push(...newLines);
|
||||
|
||||
// Copy buffer data to new locations, this needs to happen backwards to do in-place
|
||||
let destLineIndex = destLineLengths.length - 1; // Math.floor(cellsNeeded / newCols);
|
||||
let destCol = destLineLengths[destLineIndex]; // cellsNeeded % newCols;
|
||||
if (destCol === 0) {
|
||||
destLineIndex--;
|
||||
destCol = destLineLengths[destLineIndex];
|
||||
}
|
||||
let srcLineIndex = wrappedLines.length - linesToAdd - 1;
|
||||
let srcCol = lastLineLength;
|
||||
while (srcLineIndex >= 0) {
|
||||
const cellsToCopy = Math.min(srcCol, destCol);
|
||||
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol - cellsToCopy, destCol - cellsToCopy, cellsToCopy, true);
|
||||
destCol -= cellsToCopy;
|
||||
if (destCol === 0) {
|
||||
destLineIndex--;
|
||||
destCol = destLineLengths[destLineIndex];
|
||||
}
|
||||
srcCol -= cellsToCopy;
|
||||
if (srcCol === 0) {
|
||||
srcLineIndex--;
|
||||
const wrappedLinesIndex = Math.max(srcLineIndex, 0);
|
||||
srcCol = getWrappedLineTrimmedLength(wrappedLines, wrappedLinesIndex, this._cols);
|
||||
}
|
||||
}
|
||||
|
||||
// Null out the end of the line ends if a wide character wrapped to the following line
|
||||
for (let i = 0; i < wrappedLines.length; i++) {
|
||||
if (destLineLengths[i] < newCols) {
|
||||
wrappedLines[i].setCell(destLineLengths[i], nullCell);
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust viewport as needed
|
||||
let viewportAdjustments = linesToAdd - trimmedLines;
|
||||
while (viewportAdjustments-- > 0) {
|
||||
if (this.ybase === 0) {
|
||||
if (this.y < newRows - 1) {
|
||||
this.y++;
|
||||
this.lines.pop();
|
||||
} else {
|
||||
this.ybase++;
|
||||
this.ydisp++;
|
||||
}
|
||||
} else {
|
||||
// Ensure ybase does not exceed its maximum value
|
||||
if (this.ybase < Math.min(this.lines.maxLength, this.lines.length + countToInsert) - newRows) {
|
||||
if (this.ybase === this.ydisp) {
|
||||
this.ydisp++;
|
||||
}
|
||||
this.ybase++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rearrange lines in the buffer if there are any insertions, this is done at the end rather
|
||||
// than earlier so that it's a single O(n) pass through the buffer, instead of O(n^2) from many
|
||||
// costly calls to CircularList.splice.
|
||||
if (toInsert.length > 0) {
|
||||
// Record buffer insert events and then play them back backwards so that the indexes are
|
||||
// correct
|
||||
const insertEvents: IInsertEvent[] = [];
|
||||
|
||||
// Record original lines so they don't get overridden when we rearrange the list
|
||||
const originalLines: BufferLine[] = [];
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
originalLines.push(this.lines.get(i) as BufferLine);
|
||||
}
|
||||
const originalLinesLength = this.lines.length;
|
||||
|
||||
let originalLineIndex = originalLinesLength - 1;
|
||||
let nextToInsertIndex = 0;
|
||||
let nextToInsert = toInsert[nextToInsertIndex];
|
||||
this.lines.length = Math.min(this.lines.maxLength, this.lines.length + countToInsert);
|
||||
let countInsertedSoFar = 0;
|
||||
for (let i = Math.min(this.lines.maxLength - 1, originalLinesLength + countToInsert - 1); i >= 0; i--) {
|
||||
if (nextToInsert && nextToInsert.start > originalLineIndex + countInsertedSoFar) {
|
||||
// Insert extra lines here, adjusting i as needed
|
||||
for (let nextI = nextToInsert.newLines.length - 1; nextI >= 0; nextI--) {
|
||||
this.lines.set(i--, nextToInsert.newLines[nextI]);
|
||||
}
|
||||
i++;
|
||||
|
||||
// Create insert events for later
|
||||
insertEvents.push({
|
||||
index: originalLineIndex + 1,
|
||||
amount: nextToInsert.newLines.length
|
||||
});
|
||||
|
||||
countInsertedSoFar += nextToInsert.newLines.length;
|
||||
nextToInsert = toInsert[++nextToInsertIndex];
|
||||
} else {
|
||||
this.lines.set(i, originalLines[originalLineIndex--]);
|
||||
}
|
||||
}
|
||||
|
||||
// Update markers
|
||||
let insertCountEmitted = 0;
|
||||
for (let i = insertEvents.length - 1; i >= 0; i--) {
|
||||
insertEvents[i].index += insertCountEmitted;
|
||||
this.lines.onInsertEmitter.fire(insertEvents[i]);
|
||||
insertCountEmitted += insertEvents[i].amount;
|
||||
}
|
||||
const amountToTrim = Math.max(0, originalLinesLength + countToInsert - this.lines.maxLength);
|
||||
if (amountToTrim > 0) {
|
||||
this.lines.onTrimEmitter.fire(amountToTrim);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// private _reflowSmallerGetLinesNeeded()
|
||||
|
||||
/**
|
||||
* Translates a string index back to a BufferIndex.
|
||||
* To get the correct buffer position the string must start at `startCol` 0
|
||||
@@ -244,14 +465,19 @@ export class Buffer implements IBuffer {
|
||||
* @param stringIndex index within the string
|
||||
* @param startCol column offset the string was retrieved from
|
||||
*/
|
||||
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
|
||||
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex {
|
||||
while (stringIndex) {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
[-1, -1];
|
||||
return [-1, -1];
|
||||
}
|
||||
for (let i = 0; i < line.length; ++i) {
|
||||
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
|
||||
const length = (trimRight) ? line.getTrimmedLength() : line.length;
|
||||
for (let i = 0; i < length; ++i) {
|
||||
if (line.get(i)[CHAR_DATA_WIDTH_INDEX]) {
|
||||
// empty cells report a string length of 0, but get replaced
|
||||
// with a whitespace in translateToString, thus replace with 1
|
||||
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length || 1;
|
||||
}
|
||||
if (stringIndex < 0) {
|
||||
return [lineIndex, i];
|
||||
}
|
||||
@@ -271,65 +497,12 @@ export class Buffer implements IBuffer {
|
||||
* @param startCol The column to start at.
|
||||
* @param endCol The column to end at.
|
||||
*/
|
||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
||||
// Get full line
|
||||
let lineString = '';
|
||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Initialize column and index values. Column values represent the actual
|
||||
// cell column, indexes represent the index in the string. Indexes are
|
||||
// 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;
|
||||
// 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++) {
|
||||
const char = line.get(i);
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (startCol >= i) {
|
||||
startIndex--;
|
||||
}
|
||||
if (endCol > i) {
|
||||
endIndex--;
|
||||
}
|
||||
} else {
|
||||
// Adjust the columns to take glyphs that are represented by multiple
|
||||
// code points into account.
|
||||
if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
if (startCol > i) {
|
||||
startIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
if (endCol > i) {
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the final end col by trimming whitespace on the right of the
|
||||
// line if needed.
|
||||
if (trimRight) {
|
||||
const rightWhitespaceIndex = lineString.search(/\s+$/);
|
||||
if (rightWhitespaceIndex !== -1) {
|
||||
endIndex = Math.min(endIndex, rightWhitespaceIndex);
|
||||
}
|
||||
// Return the empty string if only trimmed whitespace is selected
|
||||
if (endIndex <= startIndex) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
return lineString.substring(startIndex, endIndex);
|
||||
return line.translateToString(trimRight, startCol, endCol);
|
||||
}
|
||||
|
||||
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
||||
@@ -360,7 +533,7 @@ export class Buffer implements IBuffer {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
for (; i < this._terminal.cols; i += this._terminal.options.tabStopWidth) {
|
||||
for (; i < this._cols; i += this._terminal.options.tabStopWidth) {
|
||||
this.tabs[i] = true;
|
||||
}
|
||||
}
|
||||
@@ -374,7 +547,7 @@ export class Buffer implements IBuffer {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[--x] && x > 0);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,26 +558,41 @@ export class Buffer implements IBuffer {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
while (!this.tabs[++x] && x < this._cols);
|
||||
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
marker.register(this.lines.addDisposableListener('trim', amount => {
|
||||
marker.register(this.lines.onTrim(amount => {
|
||||
marker.line -= amount;
|
||||
// The marker should be disposed when the line is trimmed from the buffer
|
||||
if (marker.line < 0) {
|
||||
marker.dispose();
|
||||
}
|
||||
}));
|
||||
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
|
||||
marker.register(this.lines.onInsert(event => {
|
||||
if (marker.line >= event.index) {
|
||||
marker.line += event.amount;
|
||||
}
|
||||
}));
|
||||
marker.register(this.lines.onDelete(event => {
|
||||
// Delete the marker if it's within the range
|
||||
if (marker.line >= event.index && marker.line < event.index + event.amount) {
|
||||
marker.dispose();
|
||||
}
|
||||
|
||||
// Shift the marker if it's after the deleted range
|
||||
if (marker.line > event.index) {
|
||||
marker.line -= event.amount;
|
||||
}
|
||||
}));
|
||||
marker.register(marker.onDispose(() => 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);
|
||||
}
|
||||
|
||||
@@ -413,31 +601,6 @@ export class Buffer implements IBuffer {
|
||||
}
|
||||
}
|
||||
|
||||
export class Marker extends EventEmitter implements IMarker {
|
||||
private static _nextId = 1;
|
||||
|
||||
private _id: number = Marker._nextId++;
|
||||
public isDisposed: boolean = false;
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
constructor(
|
||||
public line: number
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
// Emit before super.dispose such that dispose listeners get a change to react
|
||||
this.emit('dispose');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator to get unwrapped content strings from the buffer.
|
||||
* The iterator returns at least the string data between the borders
|
||||
@@ -488,8 +651,7 @@ export class BufferStringIterator implements IBufferStringIterator {
|
||||
range.last = Math.min(range.last, this._buffer.lines.length);
|
||||
let result = '';
|
||||
for (let i = range.first; i <= range.last; ++i) {
|
||||
// TODO: always apply trimRight after fixing #1685
|
||||
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
|
||||
result += this._buffer.translateBufferLineToString(i, this._trimRight);
|
||||
}
|
||||
this._current = range.last + 1;
|
||||
return {range: range, content: result};
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||
|
||||
|
||||
class TestBufferLine extends BufferLine {
|
||||
public toArray(): CharData[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
result.push(this.get(i));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('BufferLine', function(): void {
|
||||
it('ctor', function(): void {
|
||||
let line: IBufferLine = new TestBufferLine(0);
|
||||
chai.expect(line.length).equals(0);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10, null, true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.get(0)).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
line = new TestBufferLine(10, [123, 'a', 456, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.get(0)).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
});
|
||||
it('insertCells', function(): void {
|
||||
const line = new TestBufferLine(3);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.insertCells(1, 3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('deleteCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.deleteCells(1, 2, [6, 'f', 0, 'f'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[5, 'e', 0, 'e'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('replaceCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.replaceCells(2, 4, [6, 'f', 0, 'f'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[2, 'b', 0, 'b'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[5, 'e', 0, 'e'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('fill', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
line.fill([123, 'z', 0, 'z'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('clone', function(): void {
|
||||
const line = new TestBufferLine(5, null, true);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
const line2 = line.clone();
|
||||
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
chai.expect(line2.isWrapped).equals(line.isWrapped);
|
||||
});
|
||||
it('copyFrom', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.set(0, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
line.set(1, [2, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
line.set(2, [3, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
line.set(3, [4, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
line.set(4, [5, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
const line2 = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
chai.expect(line2.isWrapped).equals(line.isWrapped);
|
||||
});
|
||||
it('should support combining chars', function(): void {
|
||||
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
|
||||
// --> set code to the last charCodeAt value of the string
|
||||
// Note: needs to be fixed once the string pointer is in place
|
||||
const line = new TestBufferLine(2, [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
|
||||
const line2 = new TestBufferLine(5, [1, 'a', 0, '\u0301'.charCodeAt(0)], true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
const line3 = line.clone();
|
||||
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
|
||||
});
|
||||
describe('resize', function(): void {
|
||||
it('enlarge(false)', function(): void {
|
||||
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge(true)', function(): void {
|
||||
const line = new TestBufferLine(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(true) - should apply new size', function(): void {
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) - should not apply new size', function(): void {
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + shrink(false) - should not apply new size', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + enlarge(false) to smaller than before', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(15, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(20).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + enlarge(false) to bigger than before', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(25, [1, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.toArray()).eql(Array(25).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) + resize shrink=true should enforce shrinking', function(): void {
|
||||
const line = new TestBufferLine(20, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge from 0 length', function(): void {
|
||||
const line = new TestBufferLine(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink to 0 length', function(): void {
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(false) to 0 and enlarge to different sizes', function(): void {
|
||||
const line = new TestBufferLine(10, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
line.resize(0, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(5, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], false);
|
||||
chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.resize(7, [1, 'a', 0, 'a'.charCodeAt(0)], true);
|
||||
chai.expect(line.toArray()).eql(Array(7).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,272 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||
|
||||
/**
|
||||
* Class representing a terminal line.
|
||||
*
|
||||
* @deprecated to be removed with one of the next releases
|
||||
*/
|
||||
export class BufferLineJSArray implements IBufferLine {
|
||||
protected _data: CharData[];
|
||||
public isWrapped = false;
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCharData?: CharData, isWrapped?: boolean) {
|
||||
this._data = [];
|
||||
if (!fillCharData) {
|
||||
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
for (let i = 0; i < cols; i++) {
|
||||
this._push(fillCharData); // Note: the ctor ch is not cloned (resembles old behavior)
|
||||
}
|
||||
if (isWrapped) {
|
||||
this.isWrapped = true;
|
||||
}
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
private _pop(): CharData | undefined {
|
||||
const data = this._data.pop();
|
||||
this.length = this._data.length;
|
||||
return data;
|
||||
}
|
||||
|
||||
private _push(data: CharData): void {
|
||||
this._data.push(data);
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
private _splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
|
||||
const removed = this._data.splice(start, deleteCount, ...items);
|
||||
this.length = this._data.length;
|
||||
return removed;
|
||||
}
|
||||
|
||||
public get(index: number): CharData {
|
||||
return this._data[index];
|
||||
}
|
||||
|
||||
public set(index: number, data: CharData): void {
|
||||
this._data[index] = data;
|
||||
}
|
||||
|
||||
/** insert n cells ch at pos, right cells are lost (stable length) */
|
||||
public insertCells(pos: number, n: number, ch: CharData): void {
|
||||
while (n--) {
|
||||
this._splice(pos, 0, ch);
|
||||
this._pop();
|
||||
}
|
||||
}
|
||||
|
||||
/** delete n cells at pos, right side is filled with fill (stable length) */
|
||||
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
while (n--) {
|
||||
this._splice(pos, 1);
|
||||
this._push(fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
/** replace cells from pos to pos + n - 1 with fill */
|
||||
public replaceCells(start: number, end: number, fillCharData: CharData): void {
|
||||
while (start < end && start < this.length) {
|
||||
this.set(start++, fillCharData); // Note: fill is not cloned (resembles old behavior)
|
||||
}
|
||||
}
|
||||
|
||||
/** resize line to cols filling new cells with fill */
|
||||
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
|
||||
while (this._data.length < cols) {
|
||||
this._data.push(fillCharData);
|
||||
}
|
||||
if (shrink) {
|
||||
while (this._data.length > cols) {
|
||||
this._data.pop();
|
||||
}
|
||||
}
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
public fill(fillCharData: CharData): void {
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
public copyFrom(line: BufferLineJSArray): void {
|
||||
this._data = line._data.slice(0);
|
||||
this.length = line.length;
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLineJSArray(0);
|
||||
newLine.copyFrom(this);
|
||||
return newLine;
|
||||
}
|
||||
}
|
||||
|
||||
/** typed array slots taken by one cell */
|
||||
const CELL_SIZE = 3;
|
||||
|
||||
/** cell member indices */
|
||||
const enum Cell {
|
||||
FLAGS = 0,
|
||||
STRING = 1,
|
||||
WIDTH = 2
|
||||
}
|
||||
|
||||
/**
|
||||
* Typed array based bufferline implementation.
|
||||
*/
|
||||
export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array | null = null;
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
public length: number;
|
||||
|
||||
constructor(cols: number, fillCharData?: CharData, public isWrapped: boolean = false) {
|
||||
if (!fillCharData) {
|
||||
fillCharData = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
if (cols) {
|
||||
this._data = new Uint32Array(cols * CELL_SIZE);
|
||||
for (let i = 0; i < cols; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
this.length = cols;
|
||||
}
|
||||
|
||||
public get(index: number): CharData {
|
||||
const stringData = this._data[index * CELL_SIZE + Cell.STRING];
|
||||
return [
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS],
|
||||
(stringData & 0x80000000)
|
||||
? this._combined[index]
|
||||
: (stringData) ? String.fromCharCode(stringData) : '',
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH],
|
||||
(stringData & 0x80000000)
|
||||
? this._combined[index].charCodeAt(this._combined[index].length - 1)
|
||||
: stringData
|
||||
];
|
||||
}
|
||||
|
||||
public set(index: number, value: CharData): void {
|
||||
this._data[index * CELL_SIZE + Cell.FLAGS] = value[0];
|
||||
if (value[1].length > 1) {
|
||||
this._combined[index] = value[1];
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = index | 0x80000000;
|
||||
} else {
|
||||
this._data[index * CELL_SIZE + Cell.STRING] = value[1].charCodeAt(0);
|
||||
}
|
||||
this._data[index * CELL_SIZE + Cell.WIDTH] = value[2];
|
||||
}
|
||||
|
||||
public insertCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
for (let i = this.length - pos - n - 1; i >= 0; --i) {
|
||||
this.set(pos + n + i, this.get(pos + i));
|
||||
}
|
||||
for (let i = 0; i < n; ++i) {
|
||||
this.set(pos + i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
for (let i = pos; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public deleteCells(pos: number, n: number, fillCharData: CharData): void {
|
||||
pos %= this.length;
|
||||
if (n < this.length - pos) {
|
||||
for (let i = 0; i < this.length - pos - n; ++i) {
|
||||
this.set(pos + i, this.get(pos + n + i));
|
||||
}
|
||||
for (let i = this.length - n; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
} else {
|
||||
for (let i = pos; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public replaceCells(start: number, end: number, fillCharData: CharData): void {
|
||||
while (start < end && start < this.length) {
|
||||
this.set(start++, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(cols: number, fillCharData: CharData, shrink: boolean = false): void {
|
||||
if (cols === this.length || (!shrink && cols < this.length)) {
|
||||
return;
|
||||
}
|
||||
if (cols > this.length) {
|
||||
const data = new Uint32Array(cols * CELL_SIZE);
|
||||
if (this.length) {
|
||||
if (cols * CELL_SIZE < this._data.length) {
|
||||
data.set(this._data.subarray(0, cols * CELL_SIZE));
|
||||
} else {
|
||||
data.set(this._data);
|
||||
}
|
||||
}
|
||||
this._data = data;
|
||||
for (let i = this.length; i < cols; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
} else if (shrink) {
|
||||
if (cols) {
|
||||
const data = new Uint32Array(cols * CELL_SIZE);
|
||||
data.set(this._data.subarray(0, cols * CELL_SIZE));
|
||||
this._data = data;
|
||||
} else {
|
||||
this._data = null;
|
||||
}
|
||||
}
|
||||
this.length = cols;
|
||||
}
|
||||
|
||||
/** fill a line with fillCharData */
|
||||
public fill(fillCharData: CharData): void {
|
||||
this._combined = {};
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
this.set(i, fillCharData);
|
||||
}
|
||||
}
|
||||
|
||||
/** alter to a full copy of line */
|
||||
public copyFrom(line: BufferLine): void {
|
||||
if (this.length !== line.length) {
|
||||
this._data = new Uint32Array(line._data);
|
||||
} else {
|
||||
// use high speed copy if lengths are equal
|
||||
this._data.set(line._data);
|
||||
}
|
||||
this.length = line.length;
|
||||
this._combined = {};
|
||||
for (const el in line._combined) {
|
||||
this._combined[el] = line._combined[el];
|
||||
}
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
/** create a new clone */
|
||||
public clone(): IBufferLine {
|
||||
const newLine = new BufferLine(0);
|
||||
// creation of new typed array from another is actually pretty slow :(
|
||||
// still faster than copying values one by one
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine.length = this.length;
|
||||
for (const el in this._combined) {
|
||||
newLine._combined[el] = this._combined[el];
|
||||
}
|
||||
newLine.isWrapped = this.isWrapped;
|
||||
return newLine;
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { Buffer } from './Buffer';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
|
||||
describe('BufferSet', () => {
|
||||
let terminal: ITerminal;
|
||||
@@ -15,8 +15,8 @@ describe('BufferSet', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 24;
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 24;
|
||||
terminal.options.scrollback = 1000;
|
||||
bufferSet = new BufferSet(terminal);
|
||||
});
|
||||
|
||||
+11
-7
@@ -3,25 +3,29 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, IBufferSet } from './Types';
|
||||
import { ITerminal, IBufferSet, IBuffer } from './Types';
|
||||
import { IAttributeData } from './core/Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||
* provides also utilities for working with them.
|
||||
*/
|
||||
export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
export class BufferSet implements IBufferSet {
|
||||
private _normal: Buffer;
|
||||
private _alt: Buffer;
|
||||
private _activeBuffer: Buffer;
|
||||
|
||||
|
||||
private _onBufferActivate = new EventEmitter2<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}>();
|
||||
public get onBufferActivate(): IEvent<{activeBuffer: IBuffer, inactiveBuffer: IBuffer}> { return this._onBufferActivate.event; }
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
* @param _terminal - The terminal the BufferSet will belong to
|
||||
*/
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._normal = new Buffer(this._terminal, true);
|
||||
this._normal.fillViewportRows();
|
||||
|
||||
@@ -68,7 +72,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
// when activated.
|
||||
this._alt.clear();
|
||||
this._activeBuffer = this._normal;
|
||||
this.emit('activate', {
|
||||
this._onBufferActivate.fire({
|
||||
activeBuffer: this._normal,
|
||||
inactiveBuffer: this._alt
|
||||
});
|
||||
@@ -77,7 +81,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
/**
|
||||
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
||||
*/
|
||||
public activateAltBuffer(fillAttr?: number): void {
|
||||
public activateAltBuffer(fillAttr?: IAttributeData): void {
|
||||
if (this._activeBuffer === this._alt) {
|
||||
return;
|
||||
}
|
||||
@@ -87,7 +91,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
this._alt.x = this._normal.x;
|
||||
this._alt.y = this._normal.y;
|
||||
this._activeBuffer = this._alt;
|
||||
this.emit('activate', {
|
||||
this._onBufferActivate.fire({
|
||||
activeBuffer: this._alt,
|
||||
inactiveBuffer: this._normal
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { ICharMeasure } from '../Types';
|
||||
import { ICharMeasure } from './Types';
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
|
||||
@@ -3,23 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure, ITerminalOptions } from '../Types';
|
||||
import { EventEmitter } from '../common/EventEmitter';
|
||||
import { ICharMeasure, ITerminalOptions } from './Types';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* Utility class that measures the size of a character. Measurements are done in
|
||||
* the DOM rather than with a canvas context because support for extracting the
|
||||
* height of characters is patchy across browsers.
|
||||
*/
|
||||
export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
export class CharMeasure implements ICharMeasure {
|
||||
private _document: Document;
|
||||
private _parentElement: HTMLElement;
|
||||
private _measureElement: HTMLElement;
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
private _onCharSizeChanged = new EventEmitter2<void>();
|
||||
public get onCharSizeChanged(): IEvent<void> { return this._onCharSizeChanged.event; }
|
||||
|
||||
constructor(document: Document, parentElement: HTMLElement) {
|
||||
super();
|
||||
this._document = document;
|
||||
this._parentElement = parentElement;
|
||||
this._measureElement = this._document.createElement('span');
|
||||
@@ -38,7 +40,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
}
|
||||
|
||||
public measure(options: ITerminalOptions): void {
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontFamily = options.fontFamily;
|
||||
this._measureElement.style.fontSize = `${options.fontSize}px`;
|
||||
const geometry = this._measureElement.getBoundingClientRect();
|
||||
// The element is likely currently display:none, we should retain the
|
||||
@@ -46,10 +48,11 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
||||
if (geometry.width === 0 || geometry.height === 0) {
|
||||
return;
|
||||
}
|
||||
if (this._width !== geometry.width || this._height !== geometry.height) {
|
||||
const adjustedHeight = Math.ceil(geometry.height);
|
||||
if (this._width !== geometry.width || this._height !== adjustedHeight) {
|
||||
this._width = geometry.width;
|
||||
this._height = Math.ceil(geometry.height);
|
||||
this.emit('charsizechanged');
|
||||
this._height = adjustedHeight;
|
||||
this._onCharSizeChanged.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TestTerminal } from './utils/TestUtils.test';
|
||||
import { TestTerminal } from './TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { getStringCellWidth, wcwidth } from './CharWidth';
|
||||
import { IBuffer } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine';
|
||||
|
||||
|
||||
describe('getStringCellWidth', function(): void {
|
||||
@@ -22,7 +22,7 @@ describe('getStringCellWidth', function(): void {
|
||||
for (let i = start; i < end; ++i) {
|
||||
const line = buffer.lines.get(i);
|
||||
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
|
||||
const ch = line.get(j);
|
||||
const ch = line.loadCell(j, new CellData()).getAsCharData();
|
||||
result += ch[CHAR_DATA_WIDTH_INDEX];
|
||||
// return on sentinel
|
||||
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager } from '../Types';
|
||||
import { ITerminal, ISelectionManager } from './Types';
|
||||
|
||||
interface IWindow extends Window {
|
||||
clipboardData?: {
|
||||
@@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
*/
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void {
|
||||
// Bring textarea at the cursor position
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.width = '20px';
|
||||
textarea.style.height = '20px';
|
||||
textarea.style.left = (ev.clientX - 10) + 'px';
|
||||
textarea.style.top = (ev.clientY - 10) + 'px';
|
||||
textarea.style.zIndex = '1000';
|
||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): void {
|
||||
|
||||
textarea.focus();
|
||||
// Calculate textarea position relative to the screen element
|
||||
const pos = term.screenElement.getBoundingClientRect();
|
||||
const left = ev.clientX - pos.left - 10;
|
||||
const top = ev.clientY - pos.top - 10;
|
||||
|
||||
// Bring textarea at the cursor position
|
||||
term.textarea.style.position = 'absolute';
|
||||
term.textarea.style.width = '20px';
|
||||
term.textarea.style.height = '20px';
|
||||
term.textarea.style.left = `${left}px`;
|
||||
term.textarea.style.top = `${top}px`;
|
||||
term.textarea.style.zIndex = '1000';
|
||||
|
||||
term.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;
|
||||
textarea.style.height = null;
|
||||
textarea.style.left = null;
|
||||
textarea.style.top = null;
|
||||
textarea.style.zIndex = null;
|
||||
term.textarea.style.position = null;
|
||||
term.textarea.style.width = null;
|
||||
term.textarea.style.height = null;
|
||||
term.textarea.style.left = null;
|
||||
term.textarea.style.top = null;
|
||||
term.textarea.style.zIndex = null;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
* @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, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
||||
export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, term);
|
||||
|
||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||
selectionManager.selectWordAtCursor(ev);
|
||||
}
|
||||
|
||||
// Get textarea ready to copy from the context menu
|
||||
textarea.value = selectionManager.selectionText;
|
||||
textarea.select();
|
||||
term.textarea.value = selectionManager.selectionText;
|
||||
term.textarea.select();
|
||||
}
|
||||
@@ -111,17 +111,17 @@ export class CompositionHelper {
|
||||
/**
|
||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||
* is ending.
|
||||
* @param waitForPropogation Whether to wait for events to propogate before sending
|
||||
* @param waitForPropagation Whether to wait for events to propagate before sending
|
||||
* the input. This should be false if a non-composition keystroke is entered before the
|
||||
* compositionend event is triggered, such as enter, so that the composition is send before
|
||||
* compositionend event is triggered, such as enter, so that the composition is sent before
|
||||
* the command is executed.
|
||||
*/
|
||||
private _finalizeComposition(waitForPropogation: boolean): void {
|
||||
private _finalizeComposition(waitForPropagation: boolean): void {
|
||||
this._compositionView.classList.remove('active');
|
||||
this._isComposing = false;
|
||||
this._clearTextareaPosition();
|
||||
|
||||
if (!waitForPropogation) {
|
||||
if (!waitForPropagation) {
|
||||
// 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);
|
||||
@@ -136,8 +136,8 @@ export class CompositionHelper {
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
// browsers, use a setTimeout with 0ms time to allow the native compositionend event to
|
||||
// complete. This ensures the correct character is retrieved, this solution was used
|
||||
// because:
|
||||
// complete. This ensures the correct character is retrieved.
|
||||
// This solution was used because:
|
||||
// - The compositionend event's data property is unreliable, at least on Chromium
|
||||
// - 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
|
||||
@@ -203,6 +203,8 @@ export class CompositionHelper {
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
this._compositionView.style.height = cellHeight + 'px';
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this._compositionView.style.fontFamily = this._terminal.options.fontFamily;
|
||||
this._compositionView.style.fontSize = this._terminal.options.fontSize + '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();
|
||||
|
||||
+291
-136
File diff suppressed because it is too large
Load Diff
+95
-31
@@ -4,7 +4,16 @@
|
||||
*/
|
||||
|
||||
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { utf32ToString } from './core/input/TextDecoder';
|
||||
|
||||
interface IHandlerCollection<T> {
|
||||
[key: string]: T[];
|
||||
}
|
||||
|
||||
type CsiHandler = (params: number[], collect: string) => boolean | void;
|
||||
type OscHandler = (data: string) => boolean | void;
|
||||
|
||||
/**
|
||||
* Returns an array filled with numbers between the low and high parameters (right exclusive).
|
||||
@@ -67,7 +76,8 @@ const PRINTABLES = r(0x20, 0x7f);
|
||||
const EXECUTABLES = r(0x00, 0x18);
|
||||
EXECUTABLES.push(0x19);
|
||||
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
|
||||
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
|
||||
// Pseudo-character placeholder for printable non-ascii characters.
|
||||
const NON_ASCII_PRINTABLE = 0xA0;
|
||||
|
||||
/**
|
||||
* VT500 compatible transition table.
|
||||
@@ -79,10 +89,10 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
|
||||
let state: any;
|
||||
|
||||
// table with default transition [any] --> DEFAULT_TRANSITION
|
||||
// table with default transition
|
||||
for (state in states) {
|
||||
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small
|
||||
for (let code = 0; code < 160; ++code) {
|
||||
for (let code = 0; code <= NON_ASCII_PRINTABLE; ++code) {
|
||||
table.add(code, state, ParserAction.ERROR, ParserState.GROUND);
|
||||
}
|
||||
}
|
||||
@@ -125,6 +135,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
|
||||
table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
// csi entries
|
||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
@@ -184,6 +195,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
return table;
|
||||
})();
|
||||
|
||||
@@ -192,7 +204,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
*/
|
||||
class DcsDummy implements IDcsHandler {
|
||||
hook(collect: string, params: number[], flag: number): void { }
|
||||
put(data: string, start: number, end: number): void { }
|
||||
put(data: Uint32Array, start: number, end: number): void { }
|
||||
unhook(): void { }
|
||||
}
|
||||
|
||||
@@ -218,17 +230,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
protected _collect: string;
|
||||
|
||||
// handler lookup containers
|
||||
protected _printHandler: (data: string, start: number, end: number) => void;
|
||||
protected _printHandler: (data: Uint32Array, start: number, end: number) => void;
|
||||
protected _executeHandlers: any;
|
||||
protected _csiHandlers: any;
|
||||
protected _csiHandlers: IHandlerCollection<CsiHandler>;
|
||||
protected _escHandlers: any;
|
||||
protected _oscHandlers: any;
|
||||
protected _oscHandlers: IHandlerCollection<OscHandler>;
|
||||
protected _dcsHandlers: any;
|
||||
protected _activeDcsHandler: IDcsHandler | null;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
|
||||
// fallback handlers
|
||||
protected _printHandlerFb: (data: string, start: number, end: number) => void;
|
||||
protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void;
|
||||
protected _executeHandlerFb: (code: number) => void;
|
||||
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
|
||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||
@@ -276,15 +288,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._errorHandlerFb = null;
|
||||
this._printHandler = null;
|
||||
this._executeHandlers = null;
|
||||
this._csiHandlers = null;
|
||||
this._escHandlers = null;
|
||||
this._csiHandlers = null;
|
||||
this._oscHandlers = null;
|
||||
this._dcsHandlers = null;
|
||||
this._activeDcsHandler = null;
|
||||
this._errorHandler = null;
|
||||
}
|
||||
|
||||
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
|
||||
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
|
||||
this._printHandler = callback;
|
||||
}
|
||||
clearPrintHandler(): void {
|
||||
@@ -301,8 +313,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._executeHandlerFb = callback;
|
||||
}
|
||||
|
||||
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
|
||||
const index = flag.charCodeAt(0);
|
||||
if (this._csiHandlers[index] === undefined) {
|
||||
this._csiHandlers[index] = [];
|
||||
}
|
||||
const handlerList = this._csiHandlers[index];
|
||||
handlerList.push(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(callback);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
|
||||
this._csiHandlers[flag.charCodeAt(0)] = callback;
|
||||
this._csiHandlers[flag.charCodeAt(0)] = [callback];
|
||||
}
|
||||
clearCsiHandler(flag: string): void {
|
||||
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
||||
@@ -321,8 +349,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._escHandlerFb = callback;
|
||||
}
|
||||
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
if (this._oscHandlers[ident] === undefined) {
|
||||
this._oscHandlers[ident] = [];
|
||||
}
|
||||
const handlerList = this._oscHandlers[ident];
|
||||
handlerList.push(callback);
|
||||
return {
|
||||
dispose: () => {
|
||||
const handlerIndex = handlerList.indexOf(callback);
|
||||
if (handlerIndex !== -1) {
|
||||
handlerList.splice(handlerIndex, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void {
|
||||
this._oscHandlers[ident] = callback;
|
||||
this._oscHandlers[ident] = [callback];
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||
@@ -356,7 +399,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
this._activeDcsHandler = null;
|
||||
}
|
||||
|
||||
parse(data: string): void {
|
||||
parse(data: Uint32Array, length: number): void {
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let error = false;
|
||||
@@ -371,15 +414,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
let callback: Function | null = null;
|
||||
|
||||
// process input string
|
||||
const l = data.length;
|
||||
for (let i = 0; i < l; ++i) {
|
||||
code = data.charCodeAt(i);
|
||||
for (let i = 0; i < length; ++i) {
|
||||
code = data[i];
|
||||
|
||||
// shortcut for most chars (print action)
|
||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||
print = (~print) ? print : i;
|
||||
do i++;
|
||||
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
|
||||
while (i < length && data[i] > 0x1f && data[i] < 0x80);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
@@ -391,7 +433,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
|
||||
// normal transition & action lookup
|
||||
transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION;
|
||||
transition = table[currentState << 8 | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
|
||||
switch (transition >> 4) {
|
||||
case ParserAction.PRINT:
|
||||
print = (~print) ? print : i;
|
||||
@@ -423,10 +465,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
case ParserState.GROUND:
|
||||
print = (~print) ? print : i;
|
||||
break;
|
||||
case ParserState.OSC_STRING:
|
||||
osc += String.fromCharCode(code);
|
||||
transition |= ParserState.OSC_STRING;
|
||||
break;
|
||||
case ParserState.CSI_IGNORE:
|
||||
transition |= ParserState.CSI_IGNORE;
|
||||
break;
|
||||
@@ -463,9 +501,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
}
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
callback = this._csiHandlers[code];
|
||||
if (callback) callback(params, collect);
|
||||
else this._csiHandlerFb(collect, params, code);
|
||||
// Trigger CSI Handler
|
||||
const handlers = this._csiHandlers[code];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](params, collect) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._csiHandlerFb(collect, params, code);
|
||||
}
|
||||
break;
|
||||
case ParserAction.PARAM:
|
||||
if (code === 0x3b) params.push(0);
|
||||
@@ -517,7 +564,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
osc = '';
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
osc += data.charAt(i);
|
||||
for (let j = i + 1; ; j++) {
|
||||
if (j >= length
|
||||
|| (code = data[j]) < 0x20
|
||||
|| (code > 0x7f && code <= 0x9f)) {
|
||||
osc += utf32ToString(data, i, j);
|
||||
i = j - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||
@@ -532,9 +587,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
// or with an explicit NaN OSC handler
|
||||
const identifier = parseInt(osc.substring(0, idx));
|
||||
const content = osc.substring(idx + 1);
|
||||
callback = this._oscHandlers[identifier];
|
||||
if (callback) callback(content);
|
||||
else this._oscHandlerFb(identifier, content);
|
||||
// Trigger OSC Handler
|
||||
const handlers = this._oscHandlers[identifier];
|
||||
let j = handlers ? handlers.length - 1 : -1;
|
||||
for (; j >= 0; j--) {
|
||||
// undefined or true means success and to stop bubbling
|
||||
if (handlers[j](content) !== false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j < 0) {
|
||||
this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
@@ -549,9 +613,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
|
||||
// push leftover pushable buffers to terminal
|
||||
if (currentState === ParserState.GROUND && ~print) {
|
||||
this._printHandler(data, print, data.length);
|
||||
this._printHandler(data, print, length);
|
||||
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
|
||||
dcsHandler.put(data, dcs, data.length);
|
||||
dcsHandler.put(data, dcs, length);
|
||||
}
|
||||
|
||||
// save non pushable buffers
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as puppeteer from 'puppeteer';
|
||||
import { assert } from 'chai';
|
||||
import { ITerminalOptions } from './Types';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: puppeteer.Browser;
|
||||
let page: puppeteer.Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('InputHandler Integration Tests', function(): void {
|
||||
this.timeout(20000);
|
||||
|
||||
before(async function(): Promise<any> {
|
||||
browser = await puppeteer.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1,
|
||||
slowMo: 80,
|
||||
args: [`--window-size=${width},${height}`]
|
||||
});
|
||||
page = (await browser.pages())[0];
|
||||
await page.setViewport({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
describe('CSI', () => {
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
});
|
||||
|
||||
it('ICH: Insert Ps (Blank) Character(s) (default = 1) - CSI Ps @', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('foo\\x1b[3D\\x1b[@\\n\\r')
|
||||
// Explicit
|
||||
window.term.write('bar\\x1b[3D\\x1b[4@')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(2), [' foo', ' bar']);
|
||||
});
|
||||
|
||||
it('CUU: Cursor Up Ps Times (default = 1) - CSI Ps A', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\\n\\n\\n\\n\x1b[Aa')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Ab')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(4), ['', ' b', '', 'a']);
|
||||
});
|
||||
|
||||
it('CUD: Cursor Down Ps Times (default = 1) - CSI Ps B', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\x1b[Ba')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Bb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', ' b']);
|
||||
});
|
||||
|
||||
it('CUF: Cursor Forward Ps Times (default = 1) - CSI Ps C', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\x1b[Ca')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Cb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(1), [' a b']);
|
||||
});
|
||||
|
||||
it('CUB: Cursor Backward Ps Times (default = 1) - CSI Ps D', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('foo\x1b[Da')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Db')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(1), ['fba']);
|
||||
});
|
||||
|
||||
it('CNL: Cursor Next Line Ps Times (default = 1) - CSI Ps E', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\x1b[Ea')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Eb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(4), ['', 'a', '', 'b']);
|
||||
});
|
||||
|
||||
it('CPL: Cursor Preceding Line Ps Times (default = 1) - CSI Ps F', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\\n\\n\\n\\n\x1b[Fa')
|
||||
// Explicit
|
||||
window.term.write('\x1b[2Fb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(5), ['', 'b', '', 'a', '']);
|
||||
});
|
||||
|
||||
it('CHA: Cursor Character Absolute [column] (default = [row,1]) - CSI Ps G', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('foo\x1b[Ga')
|
||||
// Explicit
|
||||
window.term.write('\x1b[10Gb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(1), ['aoo b']);
|
||||
});
|
||||
|
||||
it('CUP: Cursor Position [row;column] (default = [1,1]) - CSI Ps ; Ps H', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('foo\x1b[Ha')
|
||||
// Explicit
|
||||
window.term.write('\x1b[3;3Hb')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['aoo', '', ' b']);
|
||||
});
|
||||
|
||||
it('CHT: Cursor Forward Tabulation Ps tab stops (default = 1) - CSI Ps I', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
// Default
|
||||
window.term.write('\x1b[Ia')
|
||||
// Explicit
|
||||
window.term.write('\\n\\r\x1b[2Ib')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(2), [' a', ' b']);
|
||||
});
|
||||
|
||||
it('ED: Erase in Display, VT100 - CSI Ps J', async function(): Promise<any> {
|
||||
const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H';
|
||||
await page.evaluate(`
|
||||
// Default: Erase Below
|
||||
window.term.resize(5, 5);
|
||||
window.term.write('${fixture}\x1b[J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
|
||||
await page.evaluate(`
|
||||
// 0: Erase Below
|
||||
window.term.reset()
|
||||
window.term.write('${fixture}\x1b[0J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
|
||||
await page.evaluate(`
|
||||
// 1: Erase Above
|
||||
window.term.reset()
|
||||
window.term.write('${fixture}\x1b[1J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
|
||||
await page.evaluate(`
|
||||
// 2: Erase Saved Lines (scrollback)
|
||||
window.term.reset()
|
||||
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[3J')
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
|
||||
assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
|
||||
});
|
||||
|
||||
it('DECSED: Erase in Display, VT220 - CSI ? Ps J', async function(): Promise<any> {
|
||||
const fixture = 'abc\\n\\rdef\\n\\rghi\x1b[2;2H';
|
||||
await page.evaluate(`
|
||||
// Default: Erase Below
|
||||
window.term.resize(5, 5);
|
||||
window.term.write('${fixture}\x1b[?J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
|
||||
await page.evaluate(`
|
||||
// 0: Erase Below
|
||||
window.term.reset()
|
||||
window.term.write('${fixture}\x1b[?0J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['abc', 'd', '']);
|
||||
await page.evaluate(`
|
||||
// 1: Erase Above
|
||||
window.term.reset()
|
||||
window.term.write('${fixture}\x1b[?1J')
|
||||
`);
|
||||
assert.deepEqual(await getLinesAsArray(3), ['', ' f', 'ghi']);
|
||||
await page.evaluate(`
|
||||
// 2: Erase Saved Lines (scrollback)
|
||||
window.term.reset()
|
||||
window.term.write('1\\n2\\n3\\n4\\n5${fixture}\x1b[?3J')
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
|
||||
assert.deepEqual(await getLinesAsArray(5), [' 4', ' 5', 'abc', 'def', 'ghi']);
|
||||
});
|
||||
|
||||
describe('DSR: Device Status Report', () => {
|
||||
it('Status Report - CSI 5 n', async function(): Promise<any> {
|
||||
await page.evaluate(`
|
||||
window.term.onData(e => window.result = e);
|
||||
window.term.write('\\x1b[5n');
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.result`), '\x1b[0n');
|
||||
});
|
||||
|
||||
it('Report Cursor Position (CPR) - CSI 6 n', async function(): Promise<any> {
|
||||
await page.evaluate(`window.term.write('\\n\\nfoo')`);
|
||||
assert.deepEqual(await page.evaluate(`
|
||||
[window.term.buffer.cursorY, window.term.buffer.cursorX]
|
||||
`), [2, 3]);
|
||||
await page.evaluate(`
|
||||
window.term.onData(e => window.result = e);
|
||||
window.term.write('\\x1b[6n');
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.result`), '\x1b[3;4R');
|
||||
});
|
||||
|
||||
it('Report Cursor Position (DECXCPR) - CSI ? 6 n', async function(): Promise<any> {
|
||||
await page.evaluate(`window.term.write('\\n\\nfoo')`);
|
||||
assert.deepEqual(await page.evaluate(`
|
||||
[window.term.buffer.cursorY, window.term.buffer.cursorX]
|
||||
`), [2, 3]);
|
||||
await page.evaluate(`
|
||||
window.term.onData(e => window.result = e);
|
||||
window.term.write('\\x1b[?6n');
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.result`), '\x1b[?3;4R');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
if (options.rendererType === 'dom') {
|
||||
await page.waitForSelector('.xterm-rows');
|
||||
} else {
|
||||
await page.waitForSelector('.xterm-text-layer');
|
||||
}
|
||||
}
|
||||
|
||||
async function getLinesAsArray(count: number, start: number = 0): Promise<string[]> {
|
||||
let text = '';
|
||||
for (let i = start; i < start + count; i++) {
|
||||
text += `window.term.buffer.getLine(${i}).translateToString(true),`;
|
||||
}
|
||||
return await page.evaluate(`[${text}]`);
|
||||
}
|
||||
+298
-324
@@ -5,152 +5,32 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX, CHAR_DATA_ATTR_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { Terminal } from './Terminal';
|
||||
import { IBufferLine } from './Types';
|
||||
|
||||
|
||||
// TODO: This and the sections related to this object in associated tests can be
|
||||
// removed safely after InputHandler refactors are finished
|
||||
class OldInputHandler extends InputHandler {
|
||||
public eraseInLine(params: number[]): void {
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 2:
|
||||
this.eraseLine(this._terminal.buffer.y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public eraseInDisplay(params: number[]): void {
|
||||
let j;
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y + 1;
|
||||
for (; j < this._terminal.rows; j++) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y;
|
||||
while (j--) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
j = this._terminal.rows;
|
||||
while (j--) this.eraseLine(j);
|
||||
break;
|
||||
case 3:
|
||||
// Clear scrollback (everything not in viewport)
|
||||
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
|
||||
if (scrollBackSize > 0) {
|
||||
this._terminal.buffer.lines.trimStart(scrollBackSize);
|
||||
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||
// Force a scroll event to refresh viewport
|
||||
this._terminal.emit('scroll', 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||
* @param x The column from which to start erasing to the end of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseRight(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the start of the line (left).
|
||||
* @param x The column from which to start erasing to the start of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseLeft(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all content in the given line
|
||||
* @param y The line to erase all of its contents.
|
||||
*/
|
||||
public eraseLine(y: number): void {
|
||||
this.eraseRight(0, y);
|
||||
}
|
||||
|
||||
public insertChars(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param < 1) param = 1;
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
let j = buffer.x;
|
||||
while (param-- && j < this._terminal.cols) {
|
||||
buffer.lines.get(row).insertCells(j++, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
}
|
||||
|
||||
public deleteChars(params: number[]): void {
|
||||
let param: number = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
while (param--) {
|
||||
buffer.lines.get(row).deleteCells(buffer.x, 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
}
|
||||
}
|
||||
import { IBufferLine } from './core/Types';
|
||||
import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
|
||||
describe('InputHandler', () => {
|
||||
describe('save and restore cursor', () => {
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
terminal.buffer.x = 1;
|
||||
terminal.buffer.y = 2;
|
||||
terminal.curAttr = 3;
|
||||
terminal.curAttrData.fg = 3;
|
||||
const inputHandler = new InputHandler(terminal);
|
||||
// Save cursor position
|
||||
inputHandler.saveCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
assert.equal(terminal.curAttrData.fg, 3);
|
||||
// Change cursor position
|
||||
terminal.buffer.x = 10;
|
||||
terminal.buffer.y = 20;
|
||||
terminal.curAttr = 30;
|
||||
terminal.curAttrData.fg = 30;
|
||||
// Restore cursor position
|
||||
inputHandler.restoreCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
assert.equal(terminal.curAttrData.fg, 3);
|
||||
});
|
||||
describe('setCursorStyle', () => {
|
||||
it('should call Terminal.setOption with correct params', () => {
|
||||
@@ -208,302 +88,258 @@ describe('InputHandler', () => {
|
||||
});
|
||||
});
|
||||
describe('regression tests', function(): void {
|
||||
function lineContent(line: IBufferLine): string {
|
||||
let content = '';
|
||||
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
return content;
|
||||
}
|
||||
|
||||
function termContent(term: Terminal): string[] {
|
||||
function termContent(term: Terminal, trim: boolean): string[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
|
||||
for (let i = 0; i < term.rows; ++i) result.push(term.buffer.lines.get(i).translateToString(trim));
|
||||
return result;
|
||||
}
|
||||
|
||||
it('insertChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(term);
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// insert one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([0]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([1]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([2]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.insertChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([10]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
|
||||
});
|
||||
it('deleteChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(term);
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0);
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// delete one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([0]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '234567890');
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([1]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '34567890');
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([2]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a') + '567890');
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.deleteChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([10]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
expect(line1.translateToString(false)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(line1.translateToString(true)).equals(Array(term.cols - 9).join('a'));
|
||||
});
|
||||
it('eraseInLine', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(term);
|
||||
|
||||
// fill 6 lines to test 3 different states
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params[0] - right erase
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
|
||||
inputHandler.eraseInLine([0]);
|
||||
expect(term.buffer.lines.get(0).translateToString(false)).equals(Array(71).join('a') + ' ');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
|
||||
inputHandler.eraseInLine([1]);
|
||||
expect(term.buffer.lines.get(1).translateToString(false)).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 2;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
term.buffer.y = 3;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 4;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([2]);
|
||||
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([2]);
|
||||
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
|
||||
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
|
||||
|
||||
});
|
||||
it('eraseInDisplay', function(): void {
|
||||
const termOld = new Terminal();
|
||||
const inputHandlerOld = new OldInputHandler(termOld);
|
||||
const termNew = new Terminal();
|
||||
const inputHandlerNew = new InputHandler(termNew);
|
||||
const term = new Terminal({cols: 80, rows: 7});
|
||||
const inputHandler = new InputHandler(term);
|
||||
|
||||
// fill display with a's
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
const data = [];
|
||||
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
|
||||
expect(termContent(termOld)).eql(data);
|
||||
expect(termContent(termOld)).eql(termContent(termNew));
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params [0] - right and below erase
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([0]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([0]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 40;
|
||||
inputHandler.eraseInDisplay([0]);
|
||||
expect(termContent(term, false)).eql([
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(40 + 1).join('a') + Array(term.cols - 40 + 1).join(' '),
|
||||
Array(term.cols + 1).join(' ')
|
||||
]);
|
||||
expect(termContent(term, true)).eql([
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(term.cols + 1).join('a'),
|
||||
Array(40 + 1).join('a'),
|
||||
''
|
||||
]);
|
||||
|
||||
// reset
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params [1] - left and above
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([1]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 40;
|
||||
inputHandler.eraseInDisplay([1]);
|
||||
expect(termContent(term, false)).eql([
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
|
||||
Array(term.cols + 1).join('a')
|
||||
]);
|
||||
expect(termContent(term, true)).eql([
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
Array(41 + 1).join(' ') + Array(term.cols - 41 + 1).join('a'),
|
||||
Array(term.cols + 1).join('a')
|
||||
]);
|
||||
|
||||
// reset
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params [2] - whole screen
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([2]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([2]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 40;
|
||||
inputHandler.eraseInDisplay([2]);
|
||||
expect(termContent(term, false)).eql([
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' '),
|
||||
Array(term.cols + 1).join(' ')
|
||||
]);
|
||||
expect(termContent(term, true)).eql([
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
''
|
||||
]);
|
||||
|
||||
// reset and add a wrapped line
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
|
||||
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
|
||||
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params[1] left and above with wrap
|
||||
// confirm precondition that line 2 is wrapped
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).true;
|
||||
termNew.buffer.y = 2;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).false;
|
||||
expect(term.buffer.lines.get(2).isWrapped).true;
|
||||
term.buffer.y = 2;
|
||||
term.buffer.x = 40;
|
||||
inputHandler.eraseInDisplay([1]);
|
||||
expect(term.buffer.lines.get(2).isWrapped).false;
|
||||
|
||||
// reset and add a wrapped line
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
inputHandlerNew.parse(Array(termNew.cols + 1).join('a')); // line 0
|
||||
inputHandlerNew.parse(Array(termNew.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < termOld.rows; ++i) inputHandlerNew.parse(Array(termNew.cols + 1).join('a'));
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 0;
|
||||
inputHandler.parse(Array(term.cols + 1).join('a')); // line 0
|
||||
inputHandler.parse(Array(term.cols + 10).join('a')); // line 1 and 2
|
||||
for (let i = 3; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params[1] left and above with wrap
|
||||
// confirm precondition that line 2 is wrapped
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).true;
|
||||
termNew.buffer.y = 1;
|
||||
termNew.buffer.x = 90; // Cursor is beyond last column
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termNew.buffer.lines.get(2).isWrapped).false;
|
||||
expect(term.buffer.lines.get(2).isWrapped).true;
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 90; // Cursor is beyond last column
|
||||
inputHandler.eraseInDisplay([1]);
|
||||
expect(term.buffer.lines.get(2).isWrapped).false;
|
||||
});
|
||||
});
|
||||
it('convertEol setting', function(): void {
|
||||
// not converting
|
||||
let s = '';
|
||||
const termNotConverting = new Terminal({cols: 15, rows: 10});
|
||||
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals(' World ');
|
||||
expect(termNotConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
|
||||
expect(termNotConverting.buffer.lines.get(1).translateToString(false)).equals(' World ');
|
||||
expect(termNotConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
|
||||
expect(termNotConverting.buffer.lines.get(1).translateToString(true)).equals(' World');
|
||||
|
||||
// converting
|
||||
s = '';
|
||||
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
|
||||
(termConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('World ');
|
||||
expect(termConverting.buffer.lines.get(0).translateToString(false)).equals('Hello ');
|
||||
expect(termConverting.buffer.lines.get(1).translateToString(false)).equals('World ');
|
||||
expect(termConverting.buffer.lines.get(0).translateToString(true)).equals('Hello');
|
||||
expect(termConverting.buffer.lines.get(1).translateToString(true)).equals('World');
|
||||
});
|
||||
describe('print', () => {
|
||||
it('should not cause an infinite loop (regression test)', () => {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
inputHandler.print(String.fromCharCode(0x200B), 0, 1);
|
||||
const container = new Uint32Array(10);
|
||||
container[0] = 0x200B;
|
||||
inputHandler.print(container, 0, 1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -511,60 +347,198 @@ describe('InputHandler', () => {
|
||||
let term: Terminal;
|
||||
let handler: InputHandler;
|
||||
|
||||
function lineContent(line: IBufferLine): string {
|
||||
let content = '';
|
||||
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
return content;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
term = new Terminal();
|
||||
handler = new InputHandler(term);
|
||||
});
|
||||
it('should handle DECSET/DECRST 47 (alt screen buffer)', () => {
|
||||
handler.parse('\x1b[?47h\r\n\x1b[31mJUNK\x1b[?47lTEST');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1047 (alt screen buffer)', () => {
|
||||
handler.parse('\x1b[?1047h\r\n\x1b[31mJUNK\x1b[?1047lTEST');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal(Array(term.cols + 1).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(' TEST' + Array(term.cols - 7).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal(' TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).get(4)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).loadCell(4, new CellData()).getFgColor())).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1048 (alt screen cursor)', () => {
|
||||
handler.parse('\x1b[?1048h\r\n\x1b[31mJUNK\x1b[?1048lTEST');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal('JUNK' + Array(term.cols - 3).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('JUNK');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
// Text color of 'JUNK' should be red
|
||||
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 (alt screen buffer+cursor)', () => {
|
||||
handler.parse('\x1b[?1049h\r\n\x1b[31mJUNK\x1b[?1049lTEST');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal(Array(term.cols + 1).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 - maintains saved cursor for alt buffer', () => {
|
||||
handler.parse('\x1b[?1049h\r\n\x1b[31m\x1b[s\x1b[?1049lTEST');
|
||||
expect(lineContent(term.buffer.lines.get(0))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||
// Text color of 'TEST' should be default
|
||||
expect(term.buffer.lines.get(0).get(0)[CHAR_DATA_ATTR_INDEX]).to.equal(DEFAULT_ATTR);
|
||||
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||
handler.parse('\x1b[?1049h\x1b[uTEST');
|
||||
expect(lineContent(term.buffer.lines.get(1))).to.equal('TEST' + Array(term.cols - 3).join(' '));
|
||||
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST');
|
||||
// Text color of 'TEST' should be red
|
||||
expect((term.buffer.lines.get(1).get(0)[CHAR_DATA_ATTR_INDEX] >> 9) & 0x1ff).to.equal(1);
|
||||
expect((term.buffer.lines.get(1).loadCell(0, new CellData()).getFgColor())).to.equal(1);
|
||||
});
|
||||
it('should handle DECSET/DECRST 1049 - clears alt buffer with erase attributes', () => {
|
||||
handler.parse('\x1b[42m\x1b[?1049h');
|
||||
// Buffer should be filled with green background
|
||||
expect(term.buffer.lines.get(20).get(10)[CHAR_DATA_ATTR_INDEX] & 0x1ff).to.equal(2);
|
||||
expect(term.buffer.lines.get(20).loadCell(10, new CellData()).getBgColor()).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('text attributes', () => {
|
||||
let term: TestTerminal;
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal();
|
||||
});
|
||||
it('bold', () => {
|
||||
term.writeSync('\x1b[1m');
|
||||
assert.equal(!!term.curAttrData.isBold(), true);
|
||||
term.writeSync('\x1b[22m');
|
||||
assert.equal(!!term.curAttrData.isBold(), false);
|
||||
});
|
||||
it('dim', () => {
|
||||
term.writeSync('\x1b[2m');
|
||||
assert.equal(!!term.curAttrData.isDim(), true);
|
||||
term.writeSync('\x1b[22m');
|
||||
assert.equal(!!term.curAttrData.isDim(), false);
|
||||
});
|
||||
it('italic', () => {
|
||||
term.writeSync('\x1b[3m');
|
||||
assert.equal(!!term.curAttrData.isItalic(), true);
|
||||
term.writeSync('\x1b[23m');
|
||||
assert.equal(!!term.curAttrData.isItalic(), false);
|
||||
});
|
||||
it('underline', () => {
|
||||
term.writeSync('\x1b[4m');
|
||||
assert.equal(!!term.curAttrData.isUnderline(), true);
|
||||
term.writeSync('\x1b[24m');
|
||||
assert.equal(!!term.curAttrData.isUnderline(), false);
|
||||
});
|
||||
it('blink', () => {
|
||||
term.writeSync('\x1b[5m');
|
||||
assert.equal(!!term.curAttrData.isBlink(), true);
|
||||
term.writeSync('\x1b[25m');
|
||||
assert.equal(!!term.curAttrData.isBlink(), false);
|
||||
});
|
||||
it('inverse', () => {
|
||||
term.writeSync('\x1b[7m');
|
||||
assert.equal(!!term.curAttrData.isInverse(), true);
|
||||
term.writeSync('\x1b[27m');
|
||||
assert.equal(!!term.curAttrData.isInverse(), false);
|
||||
});
|
||||
it('invisible', () => {
|
||||
term.writeSync('\x1b[8m');
|
||||
assert.equal(!!term.curAttrData.isInvisible(), true);
|
||||
term.writeSync('\x1b[28m');
|
||||
assert.equal(!!term.curAttrData.isInvisible(), false);
|
||||
});
|
||||
it('colormode palette 16', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
// lower 8 colors
|
||||
for (let i = 0; i < 8; ++i) {
|
||||
term.writeSync(`\x1b[${i + 30};${i + 40}m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getFgColor(), i);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getBgColor(), i);
|
||||
}
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
});
|
||||
it('colormode palette 256', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
// lower 8 colors
|
||||
for (let i = 0; i < 256; ++i) {
|
||||
term.writeSync(`\x1b[38;5;${i};48;5;${i}m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getFgColor(), i);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getBgColor(), i);
|
||||
}
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getFgColor(), -1);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColor(), -1);
|
||||
});
|
||||
it('colormode RGB', () => {
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0); // DEFAULT
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0); // DEFAULT
|
||||
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_RGB);
|
||||
assert.equal(term.curAttrData.getFgColor(), 1 << 16 | 2 << 8 | 3);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [1, 2, 3]);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_RGB);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getBgColor()), [4, 5, 6]);
|
||||
// reset to DEFAULT
|
||||
term.writeSync(`\x1b[39;49m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getFgColor(), -1);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), 0);
|
||||
assert.equal(term.curAttrData.getBgColor(), -1);
|
||||
});
|
||||
it('colormode transition RGB to 256', () => {
|
||||
// enter RGB for FG and BG
|
||||
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
|
||||
// enter 256 for FG and BG
|
||||
term.writeSync(`\x1b[38;5;255;48;5;255m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getFgColor(), 255);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getBgColor(), 255);
|
||||
});
|
||||
it('colormode transition RGB to 16', () => {
|
||||
// enter RGB for FG and BG
|
||||
term.writeSync(`\x1b[38;2;1;2;3;48;2;4;5;6m`);
|
||||
// enter 16 for FG and BG
|
||||
term.writeSync(`\x1b[37;47m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getFgColor(), 7);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getBgColor(), 7);
|
||||
});
|
||||
it('colormode transition 16 to 256', () => {
|
||||
// enter 16 for FG and BG
|
||||
term.writeSync(`\x1b[37;47m`);
|
||||
// enter 256 for FG and BG
|
||||
term.writeSync(`\x1b[38;5;255;48;5;255m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getFgColor(), 255);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P256);
|
||||
assert.equal(term.curAttrData.getBgColor(), 255);
|
||||
});
|
||||
it('colormode transition 256 to 16', () => {
|
||||
// enter 256 for FG and BG
|
||||
term.writeSync(`\x1b[38;5;255;48;5;255m`);
|
||||
// enter 16 for FG and BG
|
||||
term.writeSync(`\x1b[37;47m`);
|
||||
assert.equal(term.curAttrData.getFgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getFgColor(), 7);
|
||||
assert.equal(term.curAttrData.getBgColorMode(), Attributes.CM_P16);
|
||||
assert.equal(term.curAttrData.getBgColor(), 7);
|
||||
});
|
||||
it('should zero missing RGB values', () => {
|
||||
term.writeSync(`\x1b[38;2;1;2;3m`);
|
||||
term.writeSync(`\x1b[38;2;5m`);
|
||||
assert.deepEqual(AttributeData.toColorRGB(term.curAttrData.getFgColor()), [5, 0, 0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+205
-189
@@ -4,15 +4,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IBuffer, IInputHandlingTerminal } from './Types';
|
||||
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types';
|
||||
import { C0, C1 } from './common/data/EscapeSequences';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX, DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
|
||||
import { FLAGS } from './renderer/Types';
|
||||
import { wcwidth } from './CharWidth';
|
||||
import { EscapeSequenceParser } from './EscapeSequenceParser';
|
||||
import { ICharset } from './core/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { concat } from './common/TypedArrayUtils';
|
||||
import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from './core/input/TextDecoder';
|
||||
import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* Map collect to glevel. Used in `selectCharset`.
|
||||
@@ -24,26 +26,6 @@ const GLEVEL: {[key: string]: number} = {'(': 0, ')': 1, '*': 2, '+': 3, '-': 1,
|
||||
* DCS subparser implementations
|
||||
*/
|
||||
|
||||
/**
|
||||
* DCS + q Pt ST (xterm)
|
||||
* Request Terminfo String
|
||||
* not supported
|
||||
*/
|
||||
class RequestTerminfo implements IDcsHandler {
|
||||
private _data: string;
|
||||
constructor(private _terminal: any) { }
|
||||
hook(collect: string, params: number[], flag: number): void {
|
||||
this._data = '';
|
||||
}
|
||||
put(data: string, start: number, end: number): void {
|
||||
this._data += data.substring(start, end);
|
||||
}
|
||||
unhook(): void {
|
||||
// invalid: DCS 0 + r Pt ST
|
||||
this._terminal.handler(`${C0.ESC}P0+r${this._data}${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DCS $ q Pt ST
|
||||
* DECRQSS (https://vt100.net/docs/vt510-rm/DECRQSS.html)
|
||||
@@ -51,21 +33,22 @@ class RequestTerminfo implements IDcsHandler {
|
||||
* Response: DECRPSS (https://vt100.net/docs/vt510-rm/DECRPSS.html)
|
||||
*/
|
||||
class DECRQSS implements IDcsHandler {
|
||||
private _data: string;
|
||||
private _data: Uint32Array = new Uint32Array(0);
|
||||
|
||||
constructor(private _terminal: any) { }
|
||||
|
||||
hook(collect: string, params: number[], flag: number): void {
|
||||
// reset data
|
||||
this._data = '';
|
||||
this._data = new Uint32Array(0);
|
||||
}
|
||||
|
||||
put(data: string, start: number, end: number): void {
|
||||
this._data += data.substring(start, end);
|
||||
put(data: Uint32Array, start: number, end: number): void {
|
||||
this._data = concat(this._data, data.subarray(start, end));
|
||||
}
|
||||
|
||||
unhook(): void {
|
||||
switch (this._data) {
|
||||
const data = utf32ToString(this._data);
|
||||
this._data = new Uint32Array(0);
|
||||
switch (data) {
|
||||
// valid: DCS 1 $ r Pt ST (xterm)
|
||||
case '"q': // DECSCA
|
||||
return this._terminal.handler(`${C0.ESC}P1$r0"q${C0.ESC}\\`);
|
||||
@@ -85,8 +68,8 @@ class DECRQSS implements IDcsHandler {
|
||||
return this._terminal.handler(`${C0.ESC}P1$r${style} q${C0.ESC}\\`);
|
||||
default:
|
||||
// invalid: DCS 0 $ r Pt ST (xterm)
|
||||
this._terminal.error('Unknown DCS $q %s', this._data);
|
||||
this._terminal.handler(`${C0.ESC}P0$r${this._data}${C0.ESC}\\`);
|
||||
this._terminal.error('Unknown DCS $q %s', data);
|
||||
this._terminal.handler(`${C0.ESC}P0$r${C0.ESC}\\`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,11 +80,17 @@ class DECRQSS implements IDcsHandler {
|
||||
* not supported
|
||||
*/
|
||||
|
||||
/**
|
||||
* DCS + p Pt ST (xterm)
|
||||
* Set Terminfo Data
|
||||
* not supported
|
||||
*/
|
||||
/**
|
||||
* DCS + q Pt ST (xterm)
|
||||
* Request Terminfo String
|
||||
* not implemented
|
||||
*/
|
||||
|
||||
/**
|
||||
* DCS + p Pt ST (xterm)
|
||||
* Set Terminfo Data
|
||||
* not supported
|
||||
*/
|
||||
|
||||
|
||||
|
||||
@@ -113,7 +102,19 @@ class DECRQSS implements IDcsHandler {
|
||||
* each function's header comment.
|
||||
*/
|
||||
export class InputHandler extends Disposable implements IInputHandler {
|
||||
private _surrogateFirst: string;
|
||||
private _parseBuffer: Uint32Array = new Uint32Array(4096);
|
||||
private _stringDecoder: StringToUtf32 = new StringToUtf32();
|
||||
private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _onCursorMove = new EventEmitter2<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onData = new EventEmitter2<string>();
|
||||
public get onData(): IEvent<string> { return this._onData.event; }
|
||||
private _onLineFeed = new EventEmitter2<void>();
|
||||
public get onLineFeed(): IEvent<void> { return this._onLineFeed.event; }
|
||||
private _onScroll = new EventEmitter2<number>();
|
||||
public get onScroll(): IEvent<number> { return this._onScroll.event; }
|
||||
|
||||
constructor(
|
||||
protected _terminal: IInputHandlingTerminal,
|
||||
@@ -123,8 +124,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
this.register(this._parser);
|
||||
|
||||
this._surrogateFirst = '';
|
||||
|
||||
/**
|
||||
* custom fallback handlers
|
||||
*/
|
||||
@@ -287,7 +286,6 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* DCS handler
|
||||
*/
|
||||
this._parser.setDcsHandler('$q', new DECRQSS(this._terminal));
|
||||
this._parser.setDcsHandler('+q', new RequestTerminfo(this._terminal));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -310,13 +308,36 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
// apply leftover surrogate high from last write
|
||||
if (this._surrogateFirst) {
|
||||
data = this._surrogateFirst + data;
|
||||
this._surrogateFirst = '';
|
||||
if (this._parseBuffer.length < data.length) {
|
||||
this._parseBuffer = new Uint32Array(data.length);
|
||||
}
|
||||
this._parser.parse(this._parseBuffer, this._stringDecoder.decode(data, this._parseBuffer));
|
||||
|
||||
buffer = this._terminal.buffer;
|
||||
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
|
||||
this._onCursorMove.fire();
|
||||
}
|
||||
}
|
||||
|
||||
public parseUtf8(data: Uint8Array): void {
|
||||
// Ensure the terminal is not disposed
|
||||
if (!this._terminal) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._parser.parse(data);
|
||||
let buffer = this._terminal.buffer;
|
||||
const cursorStartX = buffer.x;
|
||||
const cursorStartY = buffer.y;
|
||||
|
||||
// TODO: Consolidate debug/logging #1560
|
||||
if ((<any>this._terminal).debug) {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
if (this._parseBuffer.length < data.length) {
|
||||
this._parseBuffer = new Uint32Array(data.length);
|
||||
}
|
||||
this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer));
|
||||
|
||||
buffer = this._terminal.buffer;
|
||||
if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
|
||||
@@ -324,59 +345,38 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
}
|
||||
}
|
||||
|
||||
public print(data: string, start: number, end: number): void {
|
||||
let char: string;
|
||||
public print(data: Uint32Array, start: number, end: number): void {
|
||||
let code: number;
|
||||
let chWidth: number;
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const charset: ICharset = this._terminal.charset;
|
||||
const screenReaderMode: boolean = this._terminal.options.screenReaderMode;
|
||||
const cols: number = this._terminal.cols;
|
||||
const wraparoundMode: boolean = this._terminal.wraparoundMode;
|
||||
const insertMode: boolean = this._terminal.insertMode;
|
||||
const curAttr: number = this._terminal.curAttr;
|
||||
const buffer = this._terminal.buffer;
|
||||
const charset = this._terminal.charset;
|
||||
const screenReaderMode = this._terminal.options.screenReaderMode;
|
||||
const cols = this._terminal.cols;
|
||||
const wraparoundMode = this._terminal.wraparoundMode;
|
||||
const insertMode = this._terminal.insertMode;
|
||||
const curAttr = this._terminal.curAttrData;
|
||||
let bufferRow = buffer.lines.get(buffer.y + buffer.ybase);
|
||||
|
||||
this._terminal.updateRange(buffer.y);
|
||||
for (let stringPosition = start; stringPosition < end; ++stringPosition) {
|
||||
char = data.charAt(stringPosition);
|
||||
code = data.charCodeAt(stringPosition);
|
||||
|
||||
// surrogate pair handling
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
if (++stringPosition >= end) {
|
||||
// end of input:
|
||||
// handle pairs as true UTF-16 and wait for the second part
|
||||
// since we expect the input comming from a stream there is
|
||||
// a small chance that the surrogate pair got split
|
||||
// therefore we dont process the first char here, instead
|
||||
// it gets added as first char to the next processed chunk
|
||||
this._surrogateFirst = char;
|
||||
continue;
|
||||
}
|
||||
const second = data.charCodeAt(stringPosition);
|
||||
// if the second part is in surrogate pair range create the high codepoint
|
||||
// otherwise fall back to UCS-2 behavior (handle codepoints independently)
|
||||
if (0xDC00 <= second && second <= 0xDFFF) {
|
||||
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
|
||||
char += data.charAt(stringPosition);
|
||||
} else {
|
||||
stringPosition--;
|
||||
}
|
||||
}
|
||||
for (let pos = start; pos < end; ++pos) {
|
||||
code = data[pos];
|
||||
|
||||
// calculate print space
|
||||
// expensive call, therefore we save width in line buffer
|
||||
chWidth = wcwidth(code);
|
||||
|
||||
// get charset replacement character
|
||||
if (charset) {
|
||||
char = charset[char] || char;
|
||||
code = char.charCodeAt(0);
|
||||
// charset is only defined for ASCII, therefore we only
|
||||
// search for an replacement char if code < 127
|
||||
if (code < 127 && charset) {
|
||||
const ch = charset[String.fromCharCode(code)];
|
||||
if (ch) {
|
||||
code = ch.charCodeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (screenReaderMode) {
|
||||
this._terminal.emit('a11y.char', char);
|
||||
this._terminal.emit('a11y.char', stringFromCodePoint(code));
|
||||
}
|
||||
|
||||
// insert combining char at last cursor position
|
||||
@@ -385,23 +385,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// since they always follow a cell consuming char
|
||||
// therefore we can test for buffer.x to avoid overflow left
|
||||
if (!chWidth && buffer.x) {
|
||||
const chMinusOne = bufferRow.get(buffer.x - 1);
|
||||
if (chMinusOne) {
|
||||
if (!chMinusOne[CHAR_DATA_WIDTH_INDEX]) {
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
// it is save to step 2 cells back here
|
||||
// since an empty cell is only set by fullwidth chars
|
||||
const chMinusTwo = bufferRow.get(buffer.x - 2);
|
||||
if (chMinusTwo) {
|
||||
chMinusTwo[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusTwo[CHAR_DATA_CODE_INDEX] = code;
|
||||
bufferRow.set(buffer.x - 2, chMinusTwo); // must be set explicitly now
|
||||
}
|
||||
} else {
|
||||
chMinusOne[CHAR_DATA_CHAR_INDEX] += char;
|
||||
chMinusOne[CHAR_DATA_CODE_INDEX] = code;
|
||||
bufferRow.set(buffer.x - 1, chMinusOne); // must be set explicitly now
|
||||
}
|
||||
if (!bufferRow.getWidth(buffer.x - 1)) {
|
||||
// found empty cell after fullwidth, need to go 2 cells back
|
||||
// it is save to step 2 cells back here
|
||||
// since an empty cell is only set by fullwidth chars
|
||||
bufferRow.addCodepointToCell(buffer.x - 2, code);
|
||||
} else {
|
||||
bufferRow.addCodepointToCell(buffer.x - 1, code);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -440,31 +430,45 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// insert mode: move characters to right
|
||||
if (insertMode) {
|
||||
// right shift cells according to the width
|
||||
bufferRow.insertCells(buffer.x, chWidth, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
bufferRow.insertCells(buffer.x, chWidth, buffer.getNullCell(curAttr));
|
||||
// test last cell - since the last cell has only room for
|
||||
// a halfwidth char any fullwidth shifted there is lost
|
||||
// and will be set to eraseChar
|
||||
const lastCell = bufferRow.get(cols - 1);
|
||||
if (lastCell[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
bufferRow.set(cols - 1, [curAttr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
// and will be set to empty cell
|
||||
if (bufferRow.getWidth(cols - 1) === 2) {
|
||||
bufferRow.setCellFromCodePoint(cols - 1, NULL_CELL_CODE, NULL_CELL_WIDTH, curAttr.fg, curAttr.bg);
|
||||
}
|
||||
}
|
||||
|
||||
// write current char to buffer and advance cursor
|
||||
bufferRow.set(buffer.x++, [curAttr, char, chWidth, code]);
|
||||
bufferRow.setCellFromCodePoint(buffer.x++, code, chWidth, curAttr.fg, curAttr.bg);
|
||||
|
||||
// fullwidth char - also set next cell to placeholder stub and advance cursor
|
||||
// for graphemes bigger than fullwidth we can simply loop to zero
|
||||
// we already made sure above, that buffer.x + chWidth will not overflow right
|
||||
if (chWidth > 0) {
|
||||
while (--chWidth) {
|
||||
bufferRow.set(buffer.x++, [curAttr, '', 0, undefined]);
|
||||
// other than a regular empty cell a cell following a wide char has no width
|
||||
bufferRow.setCellFromCodePoint(buffer.x++, 0, 0, curAttr.fg, curAttr.bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
this._terminal.updateRange(buffer.y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward addCsiHandler from parser.
|
||||
*/
|
||||
public addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
|
||||
return this._parser.addCsiHandler(flag, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward addOscHandler from parser.
|
||||
*/
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._parser.addOscHandler(ident, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* BEL
|
||||
* Bell (Ctrl-G).
|
||||
@@ -493,12 +497,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
if (buffer.x >= this._terminal.cols) {
|
||||
buffer.x--;
|
||||
}
|
||||
/**
|
||||
* This event is emitted whenever the terminal outputs a LF or NL.
|
||||
*
|
||||
* @event linefeed
|
||||
*/
|
||||
this._terminal.emit('linefeed');
|
||||
|
||||
this._onLineFeed.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -557,7 +557,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).insertCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -731,7 +731,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
line.replaceCells(
|
||||
start,
|
||||
end,
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
);
|
||||
if (clearWrap) {
|
||||
line.isWrapped = false;
|
||||
@@ -801,7 +801,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||
// Force a scroll event to refresh viewport
|
||||
this._terminal.emit('scroll', 0);
|
||||
this._onScroll.fire(0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -854,7 +854,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1L\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
buffer.lines.splice(scrollBottomAbsolute - 1, 1);
|
||||
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
|
||||
buffer.lines.splice(row, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
@@ -884,7 +884,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// test: echo -e '\e[44m\e[1M\e[0m'
|
||||
// blankLine(true) - xterm/linux behavior
|
||||
buffer.lines.splice(row, 1);
|
||||
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttr()));
|
||||
buffer.lines.splice(j, 0, buffer.getBlankLine(this._terminal.eraseAttrData()));
|
||||
}
|
||||
|
||||
// this.maxRange();
|
||||
@@ -900,7 +900,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).deleteCells(
|
||||
this._terminal.buffer.x,
|
||||
params[0] || 1,
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
);
|
||||
this._terminal.updateRange(this._terminal.buffer.y);
|
||||
}
|
||||
@@ -916,7 +916,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 1);
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -935,7 +935,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
|
||||
while (param--) {
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 1);
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollBottom, 0, buffer.getBlankLine(DEFAULT_ATTR));
|
||||
buffer.lines.splice(buffer.ybase + buffer.scrollTop, 0, buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
// this.maxRange();
|
||||
this._terminal.updateRange(buffer.scrollTop);
|
||||
@@ -951,7 +951,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.buffer.lines.get(this._terminal.buffer.y + this._terminal.buffer.ybase).replaceCells(
|
||||
this._terminal.buffer.x,
|
||||
this._terminal.buffer.x + (params[0] || 1),
|
||||
[this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
this._terminal.buffer.getNullCell(this._terminal.eraseAttrData())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1007,9 +1007,10 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// make buffer local for faster access
|
||||
const buffer = this._terminal.buffer;
|
||||
const line = buffer.lines.get(buffer.ybase + buffer.y);
|
||||
line.loadCell(buffer.x - 1, this._workCell);
|
||||
line.replaceCells(buffer.x,
|
||||
buffer.x + (params[0] || 1),
|
||||
line.get(buffer.x - 1) || [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]
|
||||
(this._workCell.content !== undefined) ? this._workCell : buffer.getNullCell(DEFAULT_ATTR_DATA)
|
||||
);
|
||||
// FIXME: no updateRange here?
|
||||
}
|
||||
@@ -1304,8 +1305,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.vt200Mouse = params[0] === 1000;
|
||||
this._terminal.normalMouse = params[0] > 1000;
|
||||
this._terminal.mouseEvents = true;
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
this._terminal.selectionManager.disable();
|
||||
if (this._terminal.element) {
|
||||
this._terminal.element.classList.add('enable-mouse-events');
|
||||
}
|
||||
if (this._terminal.selectionManager) {
|
||||
this._terminal.selectionManager.disable();
|
||||
}
|
||||
this._terminal.log('Binding to mouse events.');
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
@@ -1343,7 +1348,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// FALL-THROUGH
|
||||
case 47: // alt screen buffer
|
||||
case 1047: // alt screen buffer
|
||||
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttr());
|
||||
this._terminal.buffers.activateAltBuffer(this._terminal.eraseAttrData());
|
||||
this._terminal.refresh(0, this._terminal.rows - 1);
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
@@ -1492,8 +1497,12 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.vt200Mouse = false;
|
||||
this._terminal.normalMouse = false;
|
||||
this._terminal.mouseEvents = false;
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
this._terminal.selectionManager.enable();
|
||||
if (this._terminal.element) {
|
||||
this._terminal.element.classList.remove('enable-mouse-events');
|
||||
}
|
||||
if (this._terminal.selectionManager) {
|
||||
this._terminal.selectionManager.enable();
|
||||
}
|
||||
break;
|
||||
case 1004: // send focusin/focusout events
|
||||
this._terminal.sendFocus = false;
|
||||
@@ -1603,127 +1612,124 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public charAttributes(params: number[]): void {
|
||||
// Optimize a single SGR0.
|
||||
if (params.length === 1 && params[0] === 0) {
|
||||
this._terminal.curAttr = DEFAULT_ATTR;
|
||||
this._terminal.curAttrData.fg = DEFAULT_ATTR_DATA.fg;
|
||||
this._terminal.curAttrData.bg = DEFAULT_ATTR_DATA.bg;
|
||||
return;
|
||||
}
|
||||
|
||||
const l = params.length;
|
||||
let flags = this._terminal.curAttr >> 18;
|
||||
let fg = (this._terminal.curAttr >> 9) & 0x1ff;
|
||||
let bg = this._terminal.curAttr & 0x1ff;
|
||||
let p;
|
||||
const attr = this._terminal.curAttrData;
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
p = params[i];
|
||||
if (p >= 30 && p <= 37) {
|
||||
// fg color 8
|
||||
fg = p - 30;
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 30);
|
||||
} else if (p >= 40 && p <= 47) {
|
||||
// bg color 8
|
||||
bg = p - 40;
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 40);
|
||||
} else if (p >= 90 && p <= 97) {
|
||||
// fg color 16
|
||||
p += 8;
|
||||
fg = p - 90;
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg |= Attributes.CM_P16 | (p - 90) | 8;
|
||||
} else if (p >= 100 && p <= 107) {
|
||||
// bg color 16
|
||||
p += 8;
|
||||
bg = p - 100;
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg |= Attributes.CM_P16 | (p - 100) | 8;
|
||||
} else if (p === 0) {
|
||||
// default
|
||||
flags = DEFAULT_ATTR >> 18;
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
// flags = 0;
|
||||
// fg = 0x1ff;
|
||||
// bg = 0x1ff;
|
||||
attr.fg = DEFAULT_ATTR_DATA.fg;
|
||||
attr.bg = DEFAULT_ATTR_DATA.bg;
|
||||
} else if (p === 1) {
|
||||
// bold text
|
||||
flags |= FLAGS.BOLD;
|
||||
attr.fg |= FgFlags.BOLD;
|
||||
} else if (p === 3) {
|
||||
// italic text
|
||||
flags |= FLAGS.ITALIC;
|
||||
attr.bg |= BgFlags.ITALIC;
|
||||
} else if (p === 4) {
|
||||
// underlined text
|
||||
flags |= FLAGS.UNDERLINE;
|
||||
attr.fg |= FgFlags.UNDERLINE;
|
||||
} else if (p === 5) {
|
||||
// blink
|
||||
flags |= FLAGS.BLINK;
|
||||
attr.fg |= FgFlags.BLINK;
|
||||
} else if (p === 7) {
|
||||
// inverse and positive
|
||||
// test with: echo -e '\e[31m\e[42mhello\e[7mworld\e[27mhi\e[m'
|
||||
flags |= FLAGS.INVERSE;
|
||||
attr.fg |= FgFlags.INVERSE;
|
||||
} else if (p === 8) {
|
||||
// invisible
|
||||
flags |= FLAGS.INVISIBLE;
|
||||
attr.fg |= FgFlags.INVISIBLE;
|
||||
} else if (p === 2) {
|
||||
// dimmed text
|
||||
flags |= FLAGS.DIM;
|
||||
attr.bg |= BgFlags.DIM;
|
||||
} else if (p === 22) {
|
||||
// not bold nor faint
|
||||
flags &= ~FLAGS.BOLD;
|
||||
flags &= ~FLAGS.DIM;
|
||||
attr.fg &= ~FgFlags.BOLD;
|
||||
attr.bg &= ~BgFlags.DIM;
|
||||
} else if (p === 23) {
|
||||
// not italic
|
||||
flags &= ~FLAGS.ITALIC;
|
||||
attr.bg &= ~BgFlags.ITALIC;
|
||||
} else if (p === 24) {
|
||||
// not underlined
|
||||
flags &= ~FLAGS.UNDERLINE;
|
||||
attr.fg &= ~FgFlags.UNDERLINE;
|
||||
} else if (p === 25) {
|
||||
// not blink
|
||||
flags &= ~FLAGS.BLINK;
|
||||
attr.fg &= ~FgFlags.BLINK;
|
||||
} else if (p === 27) {
|
||||
// not inverse
|
||||
flags &= ~FLAGS.INVERSE;
|
||||
attr.fg &= ~FgFlags.INVERSE;
|
||||
} else if (p === 28) {
|
||||
// not invisible
|
||||
flags &= ~FLAGS.INVISIBLE;
|
||||
attr.fg &= ~FgFlags.INVISIBLE;
|
||||
} else if (p === 39) {
|
||||
// reset fg
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
} else if (p === 49) {
|
||||
// reset bg
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
} else if (p === 38) {
|
||||
// fg color 256
|
||||
// fg color 256 and RGB
|
||||
if (params[i + 1] === 2) {
|
||||
i += 2;
|
||||
fg = this._terminal.matchColor(
|
||||
params[i] & 0xff,
|
||||
params[i + 1] & 0xff,
|
||||
params[i + 2] & 0xff);
|
||||
if (fg === -1) fg = 0x1ff;
|
||||
attr.fg |= Attributes.CM_RGB;
|
||||
attr.fg &= ~Attributes.RGB_MASK;
|
||||
attr.fg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
|
||||
i += 2;
|
||||
} else if (params[i + 1] === 5) {
|
||||
i += 2;
|
||||
p = params[i] & 0xff;
|
||||
fg = p;
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.fg |= Attributes.CM_P256 | p;
|
||||
}
|
||||
} else if (p === 48) {
|
||||
// bg color 256
|
||||
// bg color 256 and RGB
|
||||
if (params[i + 1] === 2) {
|
||||
i += 2;
|
||||
bg = this._terminal.matchColor(
|
||||
params[i] & 0xff,
|
||||
params[i + 1] & 0xff,
|
||||
params[i + 2] & 0xff);
|
||||
if (bg === -1) bg = 0x1ff;
|
||||
attr.bg |= Attributes.CM_RGB;
|
||||
attr.bg &= ~Attributes.RGB_MASK;
|
||||
attr.bg |= AttributeData.fromColorRGB([params[i], params[i + 1], params[i + 2]]);
|
||||
i += 2;
|
||||
} else if (params[i + 1] === 5) {
|
||||
i += 2;
|
||||
p = params[i] & 0xff;
|
||||
bg = p;
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.PCOLOR_MASK);
|
||||
attr.bg |= Attributes.CM_P256 | p;
|
||||
}
|
||||
} else if (p === 100) {
|
||||
// reset fg/bg
|
||||
fg = (DEFAULT_ATTR >> 9) & 0x1ff;
|
||||
bg = DEFAULT_ATTR & 0x1ff;
|
||||
attr.fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.fg |= DEFAULT_ATTR_DATA.fg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
attr.bg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
attr.bg |= DEFAULT_ATTR_DATA.bg & (Attributes.PCOLOR_MASK | Attributes.RGB_MASK);
|
||||
} else {
|
||||
this._terminal.error('Unknown SGR attribute: %d.', p);
|
||||
}
|
||||
}
|
||||
|
||||
this._terminal.curAttr = (flags << 18) | (fg << 9) | bg;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1754,13 +1760,13 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
switch (params[0]) {
|
||||
case 5:
|
||||
// status report
|
||||
this._terminal.emit('data', `${C0.ESC}[0n`);
|
||||
this._onData.fire(`${C0.ESC}[0n`);
|
||||
break;
|
||||
case 6:
|
||||
// cursor position
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._terminal.emit('data', `${C0.ESC}[${y};${x}R`);
|
||||
this._onData.fire(`${C0.ESC}[${y};${x}R`);
|
||||
break;
|
||||
}
|
||||
} else if (collect === '?') {
|
||||
@@ -1771,7 +1777,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
// cursor position
|
||||
const y = this._terminal.buffer.y + 1;
|
||||
const x = this._terminal.buffer.x + 1;
|
||||
this._terminal.emit('data', `${C0.ESC}[?${y};${x}R`);
|
||||
this._onData.fire(`${C0.ESC}[?${y};${x}R`);
|
||||
break;
|
||||
case 15:
|
||||
// no printer
|
||||
@@ -1810,7 +1816,7 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
this._terminal.applicationCursor = false;
|
||||
this._terminal.buffer.scrollTop = 0;
|
||||
this._terminal.buffer.scrollBottom = this._terminal.rows - 1;
|
||||
this._terminal.curAttr = DEFAULT_ATTR;
|
||||
this._terminal.curAttrData = DEFAULT_ATTR_DATA;
|
||||
this._terminal.buffer.x = this._terminal.buffer.y = 0; // ?
|
||||
this._terminal.charset = null;
|
||||
this._terminal.glevel = 0; // ??
|
||||
@@ -1857,7 +1863,9 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* CSI ? Pm r
|
||||
*/
|
||||
public setScrollRegion(params: number[], collect?: string): void {
|
||||
if (collect) return;
|
||||
if (collect) {
|
||||
return;
|
||||
}
|
||||
this._terminal.buffer.scrollTop = (params[0] || 1) - 1;
|
||||
this._terminal.buffer.scrollBottom = (params[1] && params[1] <= this._terminal.rows ? params[1] : this._terminal.rows) - 1;
|
||||
this._terminal.buffer.x = 0;
|
||||
@@ -1873,7 +1881,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public saveCursor(params: number[]): void {
|
||||
this._terminal.buffer.savedX = this._terminal.buffer.x;
|
||||
this._terminal.buffer.savedY = this._terminal.buffer.y;
|
||||
this._terminal.buffer.savedCurAttr = this._terminal.curAttr;
|
||||
this._terminal.buffer.savedCurAttrData.fg = this._terminal.curAttrData.fg;
|
||||
this._terminal.buffer.savedCurAttrData.bg = this._terminal.curAttrData.bg;
|
||||
}
|
||||
|
||||
|
||||
@@ -1885,7 +1894,8 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
public restoreCursor(params: number[]): void {
|
||||
this._terminal.buffer.x = this._terminal.buffer.savedX || 0;
|
||||
this._terminal.buffer.y = this._terminal.buffer.savedY || 0;
|
||||
this._terminal.curAttr = this._terminal.buffer.savedCurAttr || DEFAULT_ATTR;
|
||||
this._terminal.curAttrData.fg = this._terminal.buffer.savedCurAttrData.fg;
|
||||
this._terminal.curAttrData.bg = this._terminal.buffer.savedCurAttrData.bg;
|
||||
}
|
||||
|
||||
|
||||
@@ -1963,9 +1973,15 @@ export class InputHandler extends Disposable implements IInputHandler {
|
||||
* Designate G3 Character Set (VT300). C = A -> ISO Latin-1 Supplemental. - Supported?
|
||||
*/
|
||||
public selectCharset(collectAndFlag: string): void {
|
||||
if (collectAndFlag.length !== 2) return this.selectDefaultCharset();
|
||||
if (collectAndFlag[0] === '/') return; // TODO: Is this supported?
|
||||
if (collectAndFlag.length !== 2) {
|
||||
this.selectDefaultCharset();
|
||||
return;
|
||||
}
|
||||
if (collectAndFlag[0] === '/') {
|
||||
return; // TODO: Is this supported?
|
||||
}
|
||||
this._terminal.setgCharset(GLEVEL[collectAndFlag[0]], CHARSETS[collectAndFlag[1]] || DEFAULT_CHARSET);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-12
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types';
|
||||
import { IBufferLine } from './core/Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { BufferLine, CellData } from './core/buffer/BufferLine';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(terminal: ITerminal) {
|
||||
@@ -41,8 +41,8 @@ describe('Linkifier', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 100;
|
||||
terminal.rows = 10;
|
||||
(terminal as any).cols = 100;
|
||||
(terminal as any).rows = 10;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||
terminal.buffer.ydisp = 0;
|
||||
@@ -53,7 +53,7 @@ describe('Linkifier', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ describe('Linkifier', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
terminal.rows = terminal.buffer.lines.length - 1;
|
||||
(terminal as any).rows = terminal.buffer.lines.length - 1;
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
@@ -142,19 +142,19 @@ describe('Linkifier', () => {
|
||||
});
|
||||
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;
|
||||
(terminal as any).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;
|
||||
(terminal as any).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;
|
||||
(terminal as any).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;
|
||||
(terminal as any).cols = 4;
|
||||
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||
});
|
||||
});
|
||||
|
||||
+26
-15
@@ -3,17 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
|
||||
import { MouseZone } from './ui/MouseZoneManager';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types';
|
||||
import { MouseZone } from './MouseZoneManager';
|
||||
import { getStringCellWidth } from './CharWidth';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
*/
|
||||
export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
export class Linkifier implements ILinkifier {
|
||||
/**
|
||||
* The time to wait after a row is changed before it is linkified. This prevents
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
@@ -35,10 +33,16 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
|
||||
private _onLinkHover = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkHover(): IEvent<ILinkifierEvent> { return this._onLinkHover.event; }
|
||||
private _onLinkLeave = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkLeave(): IEvent<ILinkifierEvent> { return this._onLinkLeave.event; }
|
||||
private _onLinkTooltip = new EventEmitter2<ILinkifierEvent>();
|
||||
public get onLinkTooltip(): IEvent<ILinkifierEvent> { return this._onLinkTooltip.event; }
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal
|
||||
) {
|
||||
super();
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
@@ -219,15 +223,22 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + uri.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
// get the buffer index as [absolute row, col] for the match
|
||||
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
if (bufferIndex[0] < 0) {
|
||||
// invalid bufferIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||
const char = line.get(bufferIndex[1]);
|
||||
const attr = line.getFg(bufferIndex[1]);
|
||||
let fg: number | undefined;
|
||||
if (char) {
|
||||
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||
if (attr) {
|
||||
fg = (attr >> 9) & 0x1ff;
|
||||
}
|
||||
|
||||
@@ -277,18 +288,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
}
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
() => {
|
||||
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
@@ -303,7 +314,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,40 +31,34 @@ describe('MouseHelper.getCoords', () => {
|
||||
actualCellWidth: CHAR_WIDTH,
|
||||
actualCellHeight: CHAR_HEIGHT
|
||||
};
|
||||
mouseHelper = new MouseHelper(renderer);
|
||||
mouseHelper = new MouseHelper(renderer as any);
|
||||
});
|
||||
|
||||
describe('when charMeasure is not initialized', () => {
|
||||
it('should return null', () => {
|
||||
charMeasure = new MockCharMeasure();
|
||||
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when pageX/pageY are not supported', () => {
|
||||
it('should return null', () => {
|
||||
assert.equal(mouseHelper.getCoords({ pageX: undefined, pageY: undefined }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 0 }, document.createElement('div'), charMeasure, 10, 10), null);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the cell that was clicked', () => {
|
||||
let coords: [number, number];
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 2]);
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [2, 1]);
|
||||
});
|
||||
|
||||
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
||||
let coords: [number, number];
|
||||
coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [1, 1]);
|
||||
// Event are double the cols/rows
|
||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH * 20, clientY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 10, 10);
|
||||
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||
});
|
||||
});
|
||||
@@ -3,40 +3,18 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharMeasure } from '../Types';
|
||||
import { IRenderer } from '../renderer/Types';
|
||||
import { ICharMeasure, IMouseHelper } from './Types';
|
||||
import { RenderCoordinator } from './renderer/RenderCoordinator';
|
||||
|
||||
export class MouseHelper {
|
||||
constructor(private _renderer: IRenderer) {}
|
||||
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
this._renderer = renderer;
|
||||
export class MouseHelper implements IMouseHelper {
|
||||
constructor(
|
||||
private _renderCoordinator: RenderCoordinator
|
||||
) {
|
||||
}
|
||||
|
||||
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
||||
// Ignore browsers that don't support MouseEvent.pageX
|
||||
if (event.pageX === null || event.pageX === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const originalElement = element;
|
||||
let x = event.pageX;
|
||||
let y = event.pageY;
|
||||
|
||||
// Converts the coordinates from being relative to the document to being
|
||||
// relative to the terminal.
|
||||
while (element) {
|
||||
x -= element.offsetLeft;
|
||||
y -= element.offsetTop;
|
||||
element = <HTMLElement>element.offsetParent;
|
||||
}
|
||||
element = originalElement;
|
||||
while (element && element !== element.ownerDocument.body) {
|
||||
x += element.scrollLeft;
|
||||
y += element.scrollTop;
|
||||
element = <HTMLElement>element.parentElement;
|
||||
}
|
||||
return [x, y];
|
||||
public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +30,7 @@ export class MouseHelper {
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
public getCoords(event: {clientX: number, clientY: number}, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number] {
|
||||
// Coordinates cannot be measured if charMeasure has not been initialized
|
||||
if (!charMeasure.width || !charMeasure.height) {
|
||||
return null;
|
||||
@@ -63,8 +41,8 @@ export class MouseHelper {
|
||||
return null;
|
||||
}
|
||||
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight);
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderCoordinator.dimensions.actualCellWidth / 2 : 0)) / this._renderCoordinator.dimensions.actualCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / this._renderCoordinator.dimensions.actualCellHeight);
|
||||
|
||||
// Ensure coordinates are within the terminal viewport. Note that selections
|
||||
// need an addition point of precision to cover the end point (as characters
|
||||
@@ -3,10 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { Disposable } from '../common/Lifecycle';
|
||||
import { addDisposableDomListener } from './Lifecycle';
|
||||
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
|
||||
const HOVER_DURATION = 500;
|
||||
|
||||
@@ -23,11 +22,13 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
|
||||
private _areZonesActive: boolean = false;
|
||||
private _mouseMoveListener: (e: MouseEvent) => any;
|
||||
private _mouseLeaveListener: (e: MouseEvent) => any;
|
||||
private _clickListener: (e: MouseEvent) => any;
|
||||
|
||||
private _tooltipTimeout: number = null;
|
||||
private _currentZone: IMouseZone = null;
|
||||
private _lastHoverCoords: [number, number] = [null, null];
|
||||
private _initialSelectionLength: number;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
@@ -38,6 +39,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
|
||||
// These events are expensive, only listen to it when mouse zones are active
|
||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||
this._mouseLeaveListener = e => this._onMouseLeave(e);
|
||||
this._clickListener = e => this._onClick(e);
|
||||
}
|
||||
|
||||
@@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
if (!this._areZonesActive) {
|
||||
this._areZonesActive = true;
|
||||
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.addEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
@@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
if (this._areZonesActive) {
|
||||
this._areZonesActive = false;
|
||||
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||
this._terminal.element.removeEventListener('click', this._clickListener);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +157,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
// Store current terminal selection length, to check if we're performing
|
||||
// a selection operation
|
||||
this._initialSelectionLength = this._terminal.getSelection().length;
|
||||
|
||||
// Ignore the event if there are no zones active
|
||||
if (!this._areZonesActive) {
|
||||
return;
|
||||
@@ -169,10 +177,25 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
}
|
||||
}
|
||||
|
||||
private _onMouseLeave(e: MouseEvent): void {
|
||||
// Fire the hover end callback and cancel any existing timer if the mouse
|
||||
// leaves the terminal element
|
||||
if (this._currentZone) {
|
||||
this._currentZone.leaveCallback();
|
||||
this._currentZone = null;
|
||||
if (this._tooltipTimeout) {
|
||||
clearTimeout(this._tooltipTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onClick(e: MouseEvent): void {
|
||||
// Find the active zone and click it if found
|
||||
// Find the active zone and click it if found and no selection was
|
||||
// being performed
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone) {
|
||||
const currentSelectionLength = this._terminal.getSelection().length;
|
||||
|
||||
if (zone && currentSelectionLength === this._initialSelectionLength) {
|
||||
zone.clickCallback(e);
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { IBufferLine } from './core/Types';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
import { BufferLine, CellData } from './core/buffer/BufferLine';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
emit(event: string, data: any): void {}
|
||||
@@ -45,8 +46,8 @@ describe('SelectionManager', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestMockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 2;
|
||||
terminal.options.scrollback = 100;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
@@ -57,14 +58,14 @@ describe('SelectionManager', () => {
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine(text.length);
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.set(i, [0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine(chars.length);
|
||||
chars.map((c, idx) => line.set(idx, [0, c, 1, c.charCodeAt(0)]));
|
||||
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
|
||||
return line;
|
||||
}
|
||||
|
||||
@@ -119,7 +120,7 @@ describe('SelectionManager', () => {
|
||||
[null, 'o', 1, 'o'.charCodeAt(0)]
|
||||
];
|
||||
const line = new BufferLine(data.length);
|
||||
for (let i = 0; i < data.length; ++i) line.set(i, data[i]);
|
||||
for (let i = 0; i < data.length; ++i) line.setCell(i, CellData.fromCharData(data[i]));
|
||||
buffer.lines.set(0, line);
|
||||
// Ensure wide characters take up 2 columns
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
|
||||
+70
-60
@@ -3,15 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
|
||||
import { XtermListener } from './common/Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './core/Platform';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types';
|
||||
import { IBufferLine } from './core/Types';
|
||||
import { MouseHelper } from './MouseHelper';
|
||||
import * as Browser from './common/Platform';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
import { CellData } from './core/buffer/BufferLine';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||
|
||||
/**
|
||||
* The number of pixels the mouse needs to be above or below the viewport in
|
||||
@@ -67,10 +68,10 @@ export const enum SelectionMode {
|
||||
* SelectionModel, SelectionManager handles with all logic associated with
|
||||
* dealing with the selection, including handling mouse interaction, wide
|
||||
* characters and fetching the actual text within the selection. Rendering is
|
||||
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
||||
* selection is ready to be redrawn.
|
||||
* not handled by the SelectionManager but the onRedrawRequest event is fired
|
||||
* when the selection is ready to be redrawn (on an animation frame).
|
||||
*/
|
||||
export class SelectionManager extends EventEmitter implements ISelectionManager {
|
||||
export class SelectionManager implements ISelectionManager {
|
||||
protected _model: SelectionModel;
|
||||
|
||||
/**
|
||||
@@ -102,15 +103,22 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: XtermListener;
|
||||
private _trimListener: IDisposable;
|
||||
private _workCell: CellData = new CellData();
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
private _onLinuxMouseSelection = new EventEmitter2<string>();
|
||||
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
|
||||
private _onRedrawRequest = new EventEmitter2<ISelectionRedrawRequestEvent>();
|
||||
public get onRedrawRequest(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
|
||||
private _onSelectionChange = new EventEmitter2<void>();
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
this._initListeners();
|
||||
this.enable();
|
||||
|
||||
@@ -119,7 +127,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
@@ -133,14 +140,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _initListeners(): void {
|
||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||
this._trimListener = (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));
|
||||
this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +204,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
}
|
||||
} else {
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : undefined;
|
||||
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||
|
||||
// Get middle rows
|
||||
@@ -240,14 +246,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._model.clearSelection();
|
||||
this._removeMouseDownListeners();
|
||||
this.refresh();
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queues a refresh, redrawing the selection on the next opportunity.
|
||||
* @param isNewSelection Whether the selection should be registered as a new
|
||||
* @param isLinuxMouseSelection Whether the selection should be registered as a new
|
||||
* selection on Linux.
|
||||
*/
|
||||
public refresh(isNewSelection?: boolean): void {
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
// Queue the refresh for the renderer
|
||||
if (!this._refreshAnimationFrame) {
|
||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||
@@ -255,10 +262,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
||||
// we need to update the selection for middle click to paste selection.
|
||||
if (Browser.isLinux && isNewSelection) {
|
||||
if (Browser.isLinux && isLinuxMouseSelection) {
|
||||
const selectionText = this.selectionText;
|
||||
if (selectionText.length) {
|
||||
this.emit('newselection', this.selectionText);
|
||||
this._onLinuxMouseSelection.fire(this.selectionText);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,7 +276,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
*/
|
||||
private _refresh(): void {
|
||||
this._refreshAnimationFrame = null;
|
||||
this.emit('refresh', {
|
||||
this._onRedrawRequest.fire({
|
||||
start: this._model.finalSelectionStart,
|
||||
end: this._model.finalSelectionEnd,
|
||||
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
||||
@@ -318,7 +325,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
public selectAll(): void {
|
||||
this._model.isSelectAllActive = true;
|
||||
this.refresh();
|
||||
this._terminal.emit('selection');
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
|
||||
public selectLines(start: number, end: number): void {
|
||||
@@ -328,7 +335,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._model.selectionStart = [0, start];
|
||||
this._model.selectionEnd = [this._terminal.cols, end];
|
||||
this.refresh();
|
||||
this._terminal.emit('selection');
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -506,8 +513,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// If the mouse is over the second half of a wide character, adjust the
|
||||
// selection to cover the whole character
|
||||
const char = line.get(this._model.selectionStart[0]);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (line.hasWidth(this._model.selectionStart[0]) === 0) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
}
|
||||
@@ -552,7 +558,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
// If the mousemove listener is active it means that a selection is
|
||||
// currently being made, we should stop propogation to prevent mouse events
|
||||
// currently being made, we should stop propagation to prevent mouse events
|
||||
// to be sent to the pty.
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
@@ -596,8 +602,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
// selection. Note that selections at the very end of the line will never
|
||||
// have a character.
|
||||
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
||||
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
|
||||
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
}
|
||||
@@ -648,7 +653,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
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');
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -658,8 +663,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
// 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);
|
||||
if (this._trimListener) {
|
||||
this._trimListener.dispose();
|
||||
}
|
||||
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -670,16 +677,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine.get(i);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
charIndex--;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1 && coords[0] !== i) {
|
||||
} else if (length > 1 && coords[0] !== i) {
|
||||
// Emojis take up multiple characters, so adjust accordingly. For these
|
||||
// we don't want ot include the character at the column as we're
|
||||
// returning the start index in the string, not the end index.
|
||||
charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
charIndex += length - 1;
|
||||
}
|
||||
}
|
||||
return charIndex;
|
||||
@@ -739,48 +746,51 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (bufferLine.getWidth(startCol) === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
if (bufferLine.getWidth(endCol) === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
|
||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
const length = bufferLine.getString(endCol).length;
|
||||
if (length > 1) {
|
||||
rightLongCharOffset += length - 1;
|
||||
endIndex += length - 1;
|
||||
}
|
||||
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
|
||||
const char = bufferLine.get(startCol - 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
|
||||
bufferLine.loadCell(startCol - 1, this._workCell);
|
||||
const length = this._workCell.getChars().length;
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
} else if (length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
leftLongCharOffset += length - 1;
|
||||
startIndex -= length - 1;
|
||||
}
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
|
||||
const char = bufferLine.get(endCol + 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
|
||||
bufferLine.loadCell(endCol + 1, this._workCell);
|
||||
const length = this._workCell.getChars().length;
|
||||
if (this._workCell.getWidth() === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
} else if (char[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
} else if (length > 1) {
|
||||
// If the next character's string is longer than 1 char (eg. emoji),
|
||||
// adjust the index
|
||||
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
rightLongCharOffset += length - 1;
|
||||
endIndex += length - 1;
|
||||
}
|
||||
endIndex++;
|
||||
endCol++;
|
||||
@@ -814,9 +824,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||
if (followWrappedLinesAbove) {
|
||||
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||
if (previousLineWordPosition) {
|
||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||
@@ -829,9 +839,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||
if (followWrappedLinesBelow) {
|
||||
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
if (start + length === this._terminal.cols && bufferLine.getCodePoint(this._terminal.cols - 1) !== 32 /*' '*/) {
|
||||
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||
if (nextLineWordPosition) {
|
||||
length += nextLineWordPosition.length;
|
||||
@@ -894,13 +904,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
*/
|
||||
private _isCharWordSeparator(charData: CharData): boolean {
|
||||
private _isCharWordSeparator(cell: CellData): boolean {
|
||||
// Zero width characters are never separators as they are always to the
|
||||
// right of wide characters
|
||||
if (charData[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (cell.getWidth() === 0) {
|
||||
return false;
|
||||
}
|
||||
return WORD_SEPARATORS.indexOf(charData[CHAR_DATA_CHAR_INDEX]) >= 0;
|
||||
return WORD_SEPARATORS.indexOf(cell.getChars()) >= 0;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { MockTerminal } from './TestUtils.test';
|
||||
|
||||
class TestSelectionModel extends SelectionModel {
|
||||
constructor(
|
||||
@@ -23,8 +23,8 @@ describe('SelectionManager', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
(terminal as any).cols = 80;
|
||||
(terminal as any).rows = 2;
|
||||
terminal.options.scrollback = 10;
|
||||
terminal.buffers = new BufferSet(terminal);
|
||||
terminal.buffer = terminal.buffers.active;
|
||||
|
||||
@@ -13,8 +13,8 @@ import * as path from 'path';
|
||||
import * as pty from 'node-pty';
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { IViewport } from './Types';
|
||||
import { CellData, WHITESPACE_CELL_CHAR } from './core/buffer/BufferLine';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
innerWrite(): void { this._innerWrite(); }
|
||||
@@ -67,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
for (let cell = 0; cell < term.cols; ++cell) {
|
||||
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX];
|
||||
lineText += term.buffer.lines.get(line).loadCell(cell, new CellData()).getChars() || WHITESPACE_CELL_CHAR;
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
@@ -91,7 +91,8 @@ if (os.platform() !== 'win32') {
|
||||
primitivePty = (<any>pty).native.open(cols, rows);
|
||||
|
||||
/** tests */
|
||||
describe('xterm output comparison', () => {
|
||||
describe('xterm output comparison', function(): void {
|
||||
this.timeout(10000);
|
||||
let xterm: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -113,8 +114,8 @@ if (os.platform() !== 'win32') {
|
||||
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||
63, 68
|
||||
];
|
||||
// These are failing on macOS only
|
||||
if (os.platform() === 'darwin') {
|
||||
// These are failing on macOS only
|
||||
skip.push(3, 7, 11, 67);
|
||||
}
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
@@ -122,9 +123,9 @@ if (os.platform() !== 'win32') {
|
||||
continue;
|
||||
}
|
||||
((filename: string) => {
|
||||
const inFile = fs.readFileSync(filename, 'utf8');
|
||||
it(filename.split('/').slice(-1)[0], done => {
|
||||
ptyReset(() => {
|
||||
const inFile = fs.readFileSync(filename, 'utf8');
|
||||
ptyWriteRead(inFile, fromPty => {
|
||||
// uncomment this to get log from terminal
|
||||
// console.log = function(){};
|
||||
|
||||
+328
-208
@@ -5,8 +5,8 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
|
||||
import { CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -16,7 +16,7 @@ class TestTerminal extends Terminal {
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('term.js addons', () => {
|
||||
describe('Terminal', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
@@ -67,6 +67,81 @@ describe('term.js addons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('should fire the onData evnet', (done) => {
|
||||
term.onData(() => done());
|
||||
term.handler('fake');
|
||||
});
|
||||
it('should fire the onCursorMove event', (done) => {
|
||||
term.on('cursormove', () => done());
|
||||
term.write('foo');
|
||||
});
|
||||
it('should fire the onLineFeed event', (done) => {
|
||||
term.on('linefeed', () => done());
|
||||
term.write('\n');
|
||||
});
|
||||
it('should fire a scroll event when scrollback is created', (done) => {
|
||||
term.on('scroll', () => done());
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
});
|
||||
it('should fire a scroll event when scrollback is cleared', (done) => {
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
term.on('scroll', () => done());
|
||||
term.clear();
|
||||
});
|
||||
it('should fire a key event after a keypress DOM event', (done) => {
|
||||
term.onKey(e => {
|
||||
assert.equal(typeof e.key, 'string');
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
const evKeyPress = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
type: 'keypress',
|
||||
keyCode: 13
|
||||
};
|
||||
term.keyPress(evKeyPress);
|
||||
});
|
||||
it('should fire a key event after a keydown DOM event', (done) => {
|
||||
term.onKey(e => {
|
||||
assert.equal(typeof e.key, 'string');
|
||||
expect(e.domEvent).to.be.an.instanceof(Object);
|
||||
done();
|
||||
});
|
||||
const evKeyDown = <KeyboardEvent>{
|
||||
preventDefault: () => { },
|
||||
stopPropagation: () => { },
|
||||
type: 'keydown',
|
||||
keyCode: 13
|
||||
};
|
||||
term.keyDown(evKeyDown);
|
||||
});
|
||||
it('should fire the onResize event', (done) => {
|
||||
term.onResize(e => {
|
||||
expect(e).to.have.keys(['cols', 'rows']);
|
||||
assert.equal(typeof e.cols, 'number');
|
||||
assert.equal(typeof e.rows, 'number');
|
||||
done();
|
||||
});
|
||||
term.resize(1, 1);
|
||||
});
|
||||
it('should fire the onScroll event', (done) => {
|
||||
term.onScroll(e => {
|
||||
assert.equal(typeof e, 'number');
|
||||
done();
|
||||
});
|
||||
term.scroll();
|
||||
});
|
||||
it('should fire the onTitleChange event', (done) => {
|
||||
term.onTitleChange(e => {
|
||||
assert.equal(e, 'title');
|
||||
done();
|
||||
});
|
||||
term.handleTitle('title');
|
||||
});
|
||||
});
|
||||
|
||||
describe('on', () => {
|
||||
beforeEach(() => {
|
||||
term.on('key', () => { });
|
||||
@@ -84,6 +159,40 @@ describe('term.js addons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('cursormove', () => {
|
||||
it('should emit a cursormove event', (done) => {
|
||||
term.on('cursormove', () => {
|
||||
done();
|
||||
});
|
||||
term.write('foo');
|
||||
});
|
||||
});
|
||||
|
||||
describe('linefeed', () => {
|
||||
it('should emit a linefeed event', (done) => {
|
||||
term.on('linefeed', () => {
|
||||
done();
|
||||
});
|
||||
term.write('\n');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll', () => {
|
||||
it('should emit a scroll event when scrollback is created', (done) => {
|
||||
term.on('scroll', () => {
|
||||
done();
|
||||
});
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
});
|
||||
it('should emit a scroll event when scrollback is cleared', (done) => {
|
||||
term.write('\n'.repeat(INIT_ROWS));
|
||||
term.on('scroll', () => {
|
||||
done();
|
||||
});
|
||||
term.clear();
|
||||
});
|
||||
});
|
||||
|
||||
describe(`keypress (including 'key' event)`, () => {
|
||||
it('should receive a string and event object', (done) => {
|
||||
let steps = 0;
|
||||
@@ -227,7 +336,7 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
describe('setOption', () => {
|
||||
it('should set the option correctly', () => {
|
||||
it('should set option correctly', () => {
|
||||
term.setOption('cursorBlink', true);
|
||||
assert.equal(term.options.cursorBlink, true);
|
||||
term.setOption('cursorBlink', false);
|
||||
@@ -259,7 +368,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
it('should clear a buffer larger than rows', () => {
|
||||
@@ -276,7 +385,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
it('should not break the prompt when cleared twice', () => {
|
||||
@@ -289,7 +398,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR));
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -455,62 +564,62 @@ describe('term.js addons', () => {
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).loadCell(0, new CellData()).getChars(), 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,65 +630,65 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
it('should create a new line and shift everything up', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(INIT_ROWS - 1).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(INIT_ROWS - 1).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// 'a' gets pushed out of buffer
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), '');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), '');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0).set(0, [0, 'a', 0, 'a'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(1).set(0, [0, 'b', 0, 'b'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(2).set(0, [0, 'c', 0, 'c'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(3).set(0, [0, 'd', 0, 'd'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(4).set(0, [0, 'e', 0, 'e'.charCodeAt(0)]);
|
||||
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -770,116 +879,126 @@ describe('term.js addons', () => {
|
||||
it('2 characters per cell', function (): void {
|
||||
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high + String.fromCharCode(i));
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(tchar.getChars().length).eql(2);
|
||||
expect(tchar.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters at last cell', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.write(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end with autowrap', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = true;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
|
||||
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end without autowrap', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = false;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
|
||||
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('splitted surrogates', () => {
|
||||
const high = String.fromCharCode(0xD800);
|
||||
const cell = new CellData();
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high);
|
||||
term.write(String.fromCharCode(i));
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||
expect(tchar.getChars().length).eql(2);
|
||||
expect(tchar.getWidth()).eql(1);
|
||||
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('unicode - combining characters', () => {
|
||||
const cell = new CellData();
|
||||
it('café', () => {
|
||||
term.write('cafe\u0301');
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(3, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
});
|
||||
it('café - end of line', () => {
|
||||
term.buffer.x = term.cols - 1 - 3;
|
||||
term.write('cafe\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('e\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
}
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('e\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
});
|
||||
it('multiple surrogate with combined', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
}
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('unicode - fullwidth characters', () => {
|
||||
const cell = new CellData();
|
||||
it('cursor movement even', () => {
|
||||
expect(term.buffer.x).eql(0);
|
||||
term.write('¥');
|
||||
@@ -895,140 +1014,141 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
it('line of ¥ odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥');
|
||||
expect(cell.getChars().length).eql(1);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
it('line of ¥ with combining odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
it('line of ¥ with combining even', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('¥\u0301');
|
||||
expect(cell.getChars().length).eql(2);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining odd', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(1);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
it('line of surrogate fullwidth with combining even', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
term.buffer.lines.get(0).loadCell(i, cell);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
||||
expect(cell.getChars()).eql('');
|
||||
expect(cell.getChars().length).eql(0);
|
||||
expect(cell.getWidth()).eql(0);
|
||||
} else {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
}
|
||||
}
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
term.buffer.lines.get(1).loadCell(0, cell);
|
||||
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||
expect(cell.getChars().length).eql(3);
|
||||
expect(cell.getWidth()).eql(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert mode', () => {
|
||||
const cell = new CellData();
|
||||
it('halfwidth - all', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
term.buffer.x = 10;
|
||||
@@ -1036,10 +1156,10 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('abcde');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
|
||||
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
|
||||
});
|
||||
it('fullwidth - insert', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
@@ -1048,11 +1168,11 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('¥¥¥');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
|
||||
});
|
||||
it('fullwidth - right border', () => {
|
||||
term.write(Array(41).join('¥'));
|
||||
@@ -1061,14 +1181,14 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('a');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
|
||||
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
|
||||
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+344
-227
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator } from '../Types';
|
||||
import { ICircularList, XtermListener } from '../common/Types';
|
||||
import { Buffer } from '../Buffer';
|
||||
import * as Browser from '../core/Platform';
|
||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
||||
import { Terminal } from '../Terminal';
|
||||
import { IRenderer, IRenderDimensions } from './renderer/Types';
|
||||
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
|
||||
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||
import { ICircularList, XtermListener } from './common/Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import * as Browser from './common/Platform';
|
||||
import { IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
|
||||
import { Terminal } from './Terminal';
|
||||
import { AttributeData } from './core/buffer/BufferLine';
|
||||
import { IColorManager, IColorSet } from './ui/Types';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
writeSync(data: string): void {
|
||||
@@ -19,6 +22,15 @@ export class TestTerminal extends Terminal {
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
onCursorMove: IEvent<void>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onTitleChange: IEvent<string>;
|
||||
onScroll: IEvent<number>;
|
||||
onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>;
|
||||
onRender: IEvent<{ start: number; end: number; }>;
|
||||
onResize: IEvent<{ cols: number; rows: number; }>;
|
||||
markers: IMarker[];
|
||||
addMarker(cursorYOffset: number): IMarker {
|
||||
throw new Error('Method not implemented.');
|
||||
@@ -54,6 +66,12 @@ export class MockTerminal implements ITerminal {
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -66,9 +84,15 @@ export class MockTerminal implements ITerminal {
|
||||
getSelection(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getSelectionPosition(): ISelectionPosition | undefined {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
clearSelection(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
select(column: number, row: number, length: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
selectAll(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -93,6 +117,9 @@ export class MockTerminal implements ITerminal {
|
||||
write(data: string): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
writeUtf8(data: Uint8Array): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
bracketedPasteMode: boolean;
|
||||
mouseHelper: IMouseHelper;
|
||||
renderer: IRenderer;
|
||||
@@ -159,6 +186,7 @@ export class MockTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
export class MockCharMeasure implements ICharMeasure {
|
||||
onCharSizeChanged: IEvent<void>;
|
||||
width: number;
|
||||
height: number;
|
||||
measure(options: ITerminalOptions): void {
|
||||
@@ -181,7 +209,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttr: number;
|
||||
curAttrData = new AttributeData();
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -216,7 +244,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
||||
setgLevel(g: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
eraseAttr(): number {
|
||||
eraseAttrData(): IAttributeData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
eraseRight(x: number, y: number): void {
|
||||
@@ -303,7 +331,7 @@ export class MockBuffer implements IBuffer {
|
||||
scrollTop: number;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
savedCurAttr: number;
|
||||
savedCurAttrData = new AttributeData();
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
|
||||
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
||||
}
|
||||
@@ -319,7 +347,7 @@ export class MockBuffer implements IBuffer {
|
||||
setLines(lines: ICircularList<IBufferLine>): void {
|
||||
this.lines = lines;
|
||||
}
|
||||
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine {
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||
return Buffer.prototype.getBlankLine.apply(this, arguments);
|
||||
}
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
|
||||
@@ -328,9 +356,17 @@ export class MockBuffer implements IBuffer {
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number): IBufferStringIterator {
|
||||
return Buffer.prototype.iterator.apply(this, arguments);
|
||||
}
|
||||
getNullCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockRenderer implements IRenderer {
|
||||
onCanvasResize: IEvent<{ width: number; height: number; }>;
|
||||
onRender: IEvent<{ start: number; end: number; }>;
|
||||
dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -348,7 +384,9 @@ export class MockRenderer implements IRenderer {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
dimensions: IRenderDimensions;
|
||||
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
|
||||
setColors(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
onResize(cols: number, rows: number): void {}
|
||||
onCharSizeChanged(): void {}
|
||||
onBlur(): void {}
|
||||
@@ -356,9 +394,9 @@ export class MockRenderer implements IRenderer {
|
||||
onSelectionChanged(start: [number, number], end: [number, number]): void {}
|
||||
onCursorMove(): void {}
|
||||
onOptionsChanged(): void {}
|
||||
onWindowResize(devicePixelRatio: number): void {}
|
||||
onDevicePixelRatioChange(): void {}
|
||||
clear(): void {}
|
||||
refreshRows(start: number, end: number): void {}
|
||||
renderRows(start: number, end: number): void {}
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
deregisterCharacterJoiner(): boolean { return true; }
|
||||
}
|
||||
@@ -368,7 +406,7 @@ export class MockViewport implements IViewport {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
scrollBarWidth: number = 0;
|
||||
onThemeChanged(colors: IColorSet): void {
|
||||
onThemeChange(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
onWheel(ev: WheelEvent): void {
|
||||
+117
-54
@@ -3,15 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ICharset } from './core/Types';
|
||||
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
|
||||
import { ICircularList } from './common/Types';
|
||||
import { IEvent } from './common/EventEmitter2';
|
||||
import { IColorSet } from './ui/Types';
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type LineData = CharData[];
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
@@ -22,12 +21,6 @@ export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
|
||||
export type BufferIndex = [number, number];
|
||||
|
||||
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
|
||||
@@ -48,7 +41,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
curAttr: number;
|
||||
curAttrData: IAttributeData;
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -70,7 +63,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
updateRange(y: number): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttr(): number;
|
||||
eraseAttrData(): IAttributeData;
|
||||
is(term: string): boolean;
|
||||
setgCharset(g: number, charset: ICharset): void;
|
||||
resize(x: number, y: number): void;
|
||||
@@ -78,7 +71,6 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
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;
|
||||
@@ -95,7 +87,7 @@ export interface IViewport extends IDisposable {
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): void;
|
||||
onThemeChanged(colors: IColorSet): void;
|
||||
onThemeChange(colors: IColorSet): void;
|
||||
}
|
||||
|
||||
export interface ICompositionHelper {
|
||||
@@ -111,7 +103,8 @@ export interface ICompositionHelper {
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
parse(data: string): void;
|
||||
print(data: string, start: number, end: number): void;
|
||||
parseUtf8(data: Uint8Array): void;
|
||||
print(data: Uint32Array, start: number, end: number): void;
|
||||
|
||||
/** C0 BEL */ bell(): void;
|
||||
/** C0 LF */ lineFeed(): void;
|
||||
@@ -194,7 +187,7 @@ export interface ILinkMatcher {
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ILinkHoverEvent {
|
||||
export interface ILinkifierEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
@@ -203,11 +196,10 @@ export interface ILinkHoverEvent {
|
||||
fg: number;
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
screenElement: HTMLElement;
|
||||
selectionManager: ISelectionManager;
|
||||
charMeasure: ICharMeasure;
|
||||
renderer: IRenderer;
|
||||
browser: IBrowser;
|
||||
writeBuffer: string[];
|
||||
cursorHidden: boolean;
|
||||
@@ -221,10 +213,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
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;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
@@ -232,12 +220,64 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
showCursor(): void;
|
||||
}
|
||||
|
||||
// Portions of the public API that are required by the internal Terminal
|
||||
export interface IPublicTerminal extends IDisposable, IEventEmitter {
|
||||
textarea: HTMLTextAreaElement;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
writeln(data: string): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
destroy(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string): void;
|
||||
writeUtf8(data: Uint8Array): void;
|
||||
getOption(key: string): any;
|
||||
setOption(key: string, value: any): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
buffer: IBuffer;
|
||||
}
|
||||
|
||||
export interface IElementAccessor {
|
||||
element: HTMLElement;
|
||||
readonly element: HTMLElement;
|
||||
}
|
||||
|
||||
export interface ILinkifierAccessor {
|
||||
@@ -245,13 +285,16 @@ export interface ILinkifierAccessor {
|
||||
}
|
||||
|
||||
export interface IMouseHelper {
|
||||
getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number };
|
||||
}
|
||||
|
||||
export interface ICharMeasure {
|
||||
width: number;
|
||||
height: number;
|
||||
|
||||
onCharSizeChanged: IEvent<void>;
|
||||
|
||||
measure(options: ITerminalOptions): void;
|
||||
}
|
||||
|
||||
@@ -289,24 +332,28 @@ export interface IBuffer {
|
||||
hasScrollback: boolean;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
savedCurAttr: number;
|
||||
savedCurAttrData: IAttributeData;
|
||||
isCursorInViewport: boolean;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||
getWrappedRangeForLine(y: number): { first: number, last: number };
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
getBlankLine(attr: number, isWrapped?: boolean): IBufferLine;
|
||||
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
|
||||
getNullCell(attr?: IAttributeData): ICellData;
|
||||
getWhitespaceCell(attr?: IAttributeData): ICellData;
|
||||
}
|
||||
|
||||
export interface IBufferSet extends IEventEmitter {
|
||||
export interface IBufferSet {
|
||||
alt: IBuffer;
|
||||
normal: IBuffer;
|
||||
active: IBuffer;
|
||||
|
||||
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
|
||||
|
||||
activateNormalBuffer(): void;
|
||||
activateAltBuffer(fillAttr?: number): void;
|
||||
activateAltBuffer(fillAttr?: IAttributeData): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
@@ -321,7 +368,17 @@ export interface ISelectionManager {
|
||||
selectWordAtCursor(event: MouseEvent): void;
|
||||
}
|
||||
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
export interface ISelectionRedrawRequestEvent {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
columnSelectMode: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifier {
|
||||
onLinkHover: IEvent<ILinkifierEvent>;
|
||||
onLinkLeave: IEvent<ILinkifierEvent>;
|
||||
onLinkTooltip: IEvent<ILinkifierEvent>;
|
||||
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
@@ -452,18 +509,26 @@ export interface IParsingState {
|
||||
* DCS handler signature for EscapeSequenceParser.
|
||||
* EscapeSequenceParser handles DCS commands via separate
|
||||
* subparsers that get hook/unhooked and can handle
|
||||
* arbitrary amount of print data.
|
||||
* arbitrary amount of data.
|
||||
*
|
||||
* On entering a DSC sequence `hook` is called by
|
||||
* `EscapeSequenceParser`. Use it to initialize or reset
|
||||
* states needed to handle the current DCS sequence.
|
||||
* Note: A DCS parser is only instantiated once, therefore
|
||||
* you cannot rely on the ctor to reinitialize state.
|
||||
*
|
||||
* EscapeSequenceParser will call `put` several times if the
|
||||
* parsed string got splitted, therefore you might have to collect
|
||||
* `data` until `unhook` is called. `unhook` marks the end
|
||||
* of the current DCS sequence.
|
||||
* parsed data got split, therefore you might have to collect
|
||||
* `data` until `unhook` is called.
|
||||
* Note: `data` is borrowed, if you cannot process the data
|
||||
* in chunks you have to copy it, doing otherwise will lead to
|
||||
* data losses or corruption.
|
||||
*
|
||||
* `unhook` marks the end of the current DCS sequence.
|
||||
*/
|
||||
export interface IDcsHandler {
|
||||
hook(collect: string, params: number[], flag: number): void;
|
||||
put(data: string, start: number, end: number): void;
|
||||
put(data: Uint32Array, start: number, end: number): void;
|
||||
unhook(): void;
|
||||
}
|
||||
|
||||
@@ -480,9 +545,9 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
* Parse string `data`.
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
parse(data: string): void;
|
||||
parse(data: Uint32Array, length: number): void;
|
||||
|
||||
setPrintHandler(callback: (data: string, start: number, end: number) => void): void;
|
||||
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void;
|
||||
clearPrintHandler(): void;
|
||||
|
||||
setExecuteHandler(flag: string, callback: () => void): void;
|
||||
@@ -492,6 +557,8 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
|
||||
clearCsiHandler(flag: string): void;
|
||||
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
|
||||
setEscHandler(collectAndFlag: string, callback: () => void): void;
|
||||
clearEscHandler(collectAndFlag: string): void;
|
||||
@@ -509,23 +576,19 @@ export interface IEscapeSequenceParser extends IDisposable {
|
||||
clearErrorHandler(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a line in the terminal buffer.
|
||||
*/
|
||||
export interface IBufferLine {
|
||||
length: number;
|
||||
isWrapped: boolean;
|
||||
get(index: number): CharData;
|
||||
set(index: number, value: CharData): void;
|
||||
insertCells(pos: number, n: number, ch: CharData): void;
|
||||
deleteCells(pos: number, n: number, fill: CharData): void;
|
||||
replaceCells(start: number, end: number, fill: CharData): void;
|
||||
resize(cols: number, fill: CharData, shrink?: boolean): void;
|
||||
fill(fillCharData: CharData): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
export interface IMouseZoneManager extends IDisposable {
|
||||
add(zone: IMouseZone): void;
|
||||
clearAll(start?: number, end?: number): void;
|
||||
}
|
||||
|
||||
export interface IBufferLineConstructor {
|
||||
new(cols: number, fillCharData?: CharData, isWrapped?: boolean): IBufferLine;
|
||||
export interface IMouseZone {
|
||||
x1: number;
|
||||
x2: number;
|
||||
y1: number;
|
||||
y2: number;
|
||||
clickCallback: (e: MouseEvent) => any;
|
||||
hoverCallback: (e: MouseEvent) => any | undefined;
|
||||
tooltipCallback: (e: MouseEvent) => any | undefined;
|
||||
leaveCallback: () => any | undefined;
|
||||
willLinkActivate: (e: MouseEvent) => boolean;
|
||||
}
|
||||
|
||||
+14
-8
@@ -3,11 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { CharMeasure } from './CharMeasure';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { IColorSet } from './ui/Types';
|
||||
import { IRenderDimensions } from './renderer/Types';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
@@ -43,7 +44,8 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _terminal: ITerminal,
|
||||
private _viewportElement: HTMLElement,
|
||||
private _scrollArea: HTMLElement,
|
||||
private _charMeasure: CharMeasure
|
||||
private _charMeasure: CharMeasure,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -57,7 +59,11 @@ export class Viewport extends Disposable implements IViewport {
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
public onThemeChanged(colors: IColorSet): void {
|
||||
public onDimensionsChance(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
|
||||
public onThemeChange(colors: IColorSet): void {
|
||||
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||
}
|
||||
|
||||
@@ -73,9 +79,9 @@ export class Viewport extends Disposable implements IViewport {
|
||||
|
||||
private _innerRefresh(): void {
|
||||
if (this._charMeasure.height > 0) {
|
||||
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight);
|
||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this._lastRecordedBufferHeight = newBufferHeight;
|
||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||
@@ -106,7 +112,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
// If viewport height changed
|
||||
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
||||
if (this._lastRecordedViewportHeight !== this._dimensions.canvasHeight) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +131,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
// If row height changed
|
||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
if (this._dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
import { ITerminal } from './Types';
|
||||
import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './core/buffer/BufferLine';
|
||||
|
||||
export function applyWindowsMode(terminal: ITerminal): IDisposable {
|
||||
// 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.
|
||||
return terminal.onLineFeed(() => {
|
||||
const line = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y - 1);
|
||||
const lastChar = line.get(terminal.cols - 1);
|
||||
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE && lastChar[CHAR_DATA_CODE_INDEX] !== WHITESPACE_CELL_CODE) {
|
||||
const nextLine = terminal.buffer.lines.get(terminal.buffer.ybase + terminal.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export interface IAttachAddonTerminal extends Terminal {
|
||||
|
||||
__socket?: WebSocket;
|
||||
__attachSocketBuffer?: string;
|
||||
__dataListener?: IDisposable;
|
||||
|
||||
__getMessage?(ev: MessageEvent): void;
|
||||
__flushBuffer?(): void;
|
||||
|
||||
@@ -90,7 +90,8 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
|
||||
addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData);
|
||||
addonTerminal._core.register(addonTerminal.__dataListener);
|
||||
}
|
||||
|
||||
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
|
||||
@@ -119,7 +120,8 @@ function addSocketListener(socket: WebSocket, type: string, handler: (this: WebS
|
||||
*/
|
||||
export function detach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <IAttachAddonTerminal>term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
addonTerminal.__dataListener.dispose();
|
||||
addonTerminal.__dataListener = undefined;
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
"outDir": "../../../lib/addons/attach/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
"declaration": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
|
||||
@@ -39,8 +39,8 @@ export function proposeGeometry(term: Terminal): IGeometry {
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
|
||||
const geometry = {
|
||||
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
|
||||
cols: Math.floor(availableWidth / (<any>term)._core._renderCoordinator.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term)._core._renderCoordinator.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
@@ -50,7 +50,7 @@ export function fit(term: Terminal): void {
|
||||
if (geometry) {
|
||||
// Force a full render
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
(<any>term)._core.renderer.clear();
|
||||
(<any>term)._core._renderCoordinator.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||
fn = term.element.classList.add;
|
||||
}
|
||||
|
||||
fn = fn.bind(term.element.classList);
|
||||
fn('fullscreen');
|
||||
}
|
||||
|
||||
|
||||
@@ -11,13 +11,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,15 +5,8 @@
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
// TODO: Don't rely on this private API
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
selectionManager: any;
|
||||
}
|
||||
|
||||
export interface ISearchAddonTerminal extends Terminal {
|
||||
__searchHelper?: ISearchHelper;
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
|
||||
export interface ISearchHelper {
|
||||
@@ -25,6 +18,11 @@ export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
/**
|
||||
* Use this when you want the selection to expand if it still matches as the
|
||||
* user types. Note that this only affects findNext.
|
||||
*/
|
||||
incremental?: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
|
||||
@@ -4,16 +4,27 @@
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
*/
|
||||
export class SearchHelper implements ISearchHelper {
|
||||
/**
|
||||
* translateBufferLineToStringWithWrap is a fairly expensive call.
|
||||
* We memoize the calls into an array that has a time based ttl.
|
||||
* _linesCache is also invalidated when the terminal cursor moves.
|
||||
*/
|
||||
private _linesCache: string[] = null;
|
||||
private _linesCacheTimeoutId = 0;
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
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
|
||||
this._destroyLinesCache = this._destroyLinesCache.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -24,32 +35,60 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
const {incremental} = searchOptions;
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
let startCol: number = 0;
|
||||
let startRow = this._terminal.buffer.viewportY;
|
||||
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
if (this._terminal.hasSelection()) {
|
||||
// Start from the selection end if there is a selection
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
}
|
||||
// For incremental search, use existing row
|
||||
const currentSelection = this._terminal.getSelectionPosition();
|
||||
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
|
||||
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
this._initLinesCache();
|
||||
|
||||
// A row that has isWrapped = false
|
||||
let findingRow = startRow;
|
||||
// index of beginning column that _findInLine need to scan.
|
||||
let cumulativeCols = startCol;
|
||||
// If startRow is wrapped row, scan for unwrapped row above.
|
||||
// So we can start matching on wrapped line from long unwrapped line.
|
||||
while (this._terminal.buffer.getLine(findingRow).isWrapped) {
|
||||
findingRow--;
|
||||
cumulativeCols += this._terminal.cols;
|
||||
}
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
// Search startRow
|
||||
result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
for (let y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
|
||||
for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
|
||||
|
||||
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
||||
// Otherwise, reset beginning column index to zero with set new unwrapped line index
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the top to the startRow (search the whole startRow again in
|
||||
// case startCol > 0)
|
||||
if (!result) {
|
||||
for (let y = 0; y < findingRow; y++) {
|
||||
result = this._findInLine(term, y, 0, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -68,35 +107,67 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
let result: ISearchResult;
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
|
||||
let result: ISearchResult;
|
||||
const isReverseSearch = true;
|
||||
let startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1;
|
||||
let startCol = this._terminal.cols;
|
||||
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
// Start from the selection end if there is a selection
|
||||
if (this._terminal.getSelection().length !== 0) {
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
}
|
||||
if (this._terminal.hasSelection()) {
|
||||
// Start from the selection start if there is a selection
|
||||
const currentSelection = this._terminal.getSelectionPosition();
|
||||
startRow = currentSelection.startRow;
|
||||
startCol = currentSelection.startColumn;
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._initLinesCache();
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
// Search startRow
|
||||
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
|
||||
|
||||
// Search from startRow - 1 to top
|
||||
if (!result) {
|
||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
// If the line is wrapped line, increase number of columns that is needed to be scanned
|
||||
// Se we can scan on wrapped line from unwrapped line
|
||||
let cumulativeCols = this._terminal.cols;
|
||||
if (this._terminal.buffer.getLine(startRow).isWrapped) {
|
||||
cumulativeCols += startCol;
|
||||
}
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
// If the current line is wrapped line, increase scanning range,
|
||||
// preparing for scanning on unwrapped line
|
||||
if (this._terminal.buffer.getLine(y).isWrapped) {
|
||||
cumulativeCols += this._terminal.cols;
|
||||
} else {
|
||||
cumulativeCols = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Search from the bottom to startRow (search the whole startRow again in
|
||||
// case startCol > 0)
|
||||
if (!result) {
|
||||
const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;
|
||||
let cumulativeCols = this._terminal.cols;
|
||||
for (let y = searchFrom; y >= startRow; y--) {
|
||||
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
if (this._terminal.buffer.getLine(y).isWrapped) {
|
||||
cumulativeCols += this._terminal.cols;
|
||||
} else {
|
||||
cumulativeCols = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +175,36 @@ export class SearchHelper implements ISearchHelper {
|
||||
return this._selectResult(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a line cache with a ttl
|
||||
*/
|
||||
private _initLinesCache(): void {
|
||||
if (!this._linesCache) {
|
||||
this._linesCache = new Array(this._terminal.buffer.length);
|
||||
this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache());
|
||||
this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache());
|
||||
}
|
||||
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
|
||||
}
|
||||
|
||||
private _destroyLinesCache(): void {
|
||||
this._linesCache = null;
|
||||
if (this._cursorMoveListener) {
|
||||
this._cursorMoveListener.dispose();
|
||||
this._cursorMoveListener = undefined;
|
||||
}
|
||||
if (this._resizeListener) {
|
||||
this._resizeListener.dispose();
|
||||
this._resizeListener = undefined;
|
||||
}
|
||||
if (this._linesCacheTimeoutId) {
|
||||
window.clearTimeout(this._linesCacheTimeoutId);
|
||||
this._linesCacheTimeoutId = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
|
||||
* @param searchIndex starting indext of the potential whole word substring
|
||||
@@ -111,8 +212,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @param term the substring that starts at searchIndex
|
||||
*/
|
||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,65 +222,89 @@ export class SearchHelper implements ISearchHelper {
|
||||
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||
* text starts on is searched.
|
||||
* @param term The search term.
|
||||
* @param y The line to search.
|
||||
* @param row The line to start the search from.
|
||||
* @param col The column to start the search from.
|
||||
* @param searchOptions Search options.
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
|
||||
|
||||
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||
if (this._terminal.buffer.getLine(row).isWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
let searchIndex = -1;
|
||||
|
||||
if (searchOptions.regex) {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
const foundTerm = searchRegex.exec(searchStringLine);
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||
term = foundTerm[0];
|
||||
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||
if (stringLine === void 0) {
|
||||
stringLine = this.translateBufferLineToStringWithWrap(row, true);
|
||||
if (this._linesCache) {
|
||||
this._linesCache[row] = stringLine;
|
||||
}
|
||||
} else {
|
||||
searchIndex = searchStringLine.indexOf(searchTerm);
|
||||
}
|
||||
|
||||
if (searchIndex >= 0) {
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (searchIndex >= this._terminal.cols) {
|
||||
y += Math.floor(searchIndex / this._terminal.cols);
|
||||
searchIndex = searchIndex % this._terminal.cols;
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
|
||||
let resultIndex = -1;
|
||||
if (searchOptions.regex) {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
let foundTerm: RegExpExecArray;
|
||||
if (isReverseSearch) {
|
||||
// This loop will get the resultIndex of the _last_ regex match in the range 0..col
|
||||
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) {
|
||||
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||
term = foundTerm[0];
|
||||
searchRegex.lastIndex -= (term.length - 1);
|
||||
}
|
||||
} else {
|
||||
foundTerm = searchRegex.exec(searchStringLine.slice(col));
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
|
||||
term = foundTerm[0];
|
||||
}
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
|
||||
} else {
|
||||
if (isReverseSearch) {
|
||||
if (col - searchTerm.length >= 0) {
|
||||
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
|
||||
}
|
||||
} else {
|
||||
resultIndex = searchStringLine.indexOf(searchTerm, col);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultIndex >= 0) {
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (resultIndex >= this._terminal.cols) {
|
||||
row += Math.floor(resultIndex / this._terminal.cols);
|
||||
resultIndex = resultIndex % this._terminal.cols;
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = this._terminal._core.buffer.lines.get(y);
|
||||
const line = this._terminal.buffer.getLine(row);
|
||||
|
||||
for (let i = 0; i < searchIndex; i++) {
|
||||
const charData = line.get(i);
|
||||
for (let i = 0; i < resultIndex; i++) {
|
||||
const cell = line.getCell(i);
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||
const char = cell.char;
|
||||
if (char.length > 1) {
|
||||
searchIndex -= char.length - 1;
|
||||
resultIndex -= char.length - 1;
|
||||
}
|
||||
// Adjust the searchIndex for empty characters following wide unicode
|
||||
// chars (eg. CJK)
|
||||
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
|
||||
const charWidth = cell.width;
|
||||
if (charWidth === 0) {
|
||||
searchIndex++;
|
||||
resultIndex++;
|
||||
}
|
||||
}
|
||||
return {
|
||||
term,
|
||||
col: searchIndex,
|
||||
row: y
|
||||
col: resultIndex,
|
||||
row
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a buffer line to a string, including subsequent lines if they are wraps.
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
@@ -193,9 +318,9 @@ export class SearchHelper implements ISearchHelper {
|
||||
let lineWrapsToNext: boolean;
|
||||
|
||||
do {
|
||||
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
|
||||
const nextLine = this._terminal.buffer.getLine(lineIndex + 1);
|
||||
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
|
||||
lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
|
||||
lineIndex++;
|
||||
} while (lineWrapsToNext);
|
||||
|
||||
@@ -212,8 +337,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
this._terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
|
||||
this._terminal.select(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,62 @@ class MockTerminal {
|
||||
get core(): any {
|
||||
return this._core;
|
||||
}
|
||||
get buffer(): IBuffer {
|
||||
// TODO: This is a hacky workaround until we use puppeteer for addon tests
|
||||
const buffer = this._core.buffer;
|
||||
return {
|
||||
cursorY: buffer.y,
|
||||
cursorX: buffer.x,
|
||||
viewportY: buffer.ydisp,
|
||||
baseY: buffer.ybase,
|
||||
length: buffer.length,
|
||||
getLine(y: number): IBufferLine {
|
||||
return {
|
||||
isWrapped: buffer.lines.get(y) ? buffer.lines.get(y).isWrapped : false,
|
||||
getCell(x: number): IBufferCell {
|
||||
return {
|
||||
char: buffer.lines.get(y).get(x)[1/*CHAR_DATA_CHAR_INDEX*/],
|
||||
width: buffer.lines.get(y).get(x)[2/*CHAR_DATA_WIDTH_INDEX*/]
|
||||
};
|
||||
},
|
||||
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
|
||||
return buffer.translateBufferLineToString(y, trimRight);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
pushWriteData(): void {
|
||||
this._core._innerWrite();
|
||||
}
|
||||
}
|
||||
|
||||
interface IBuffer {
|
||||
readonly cursorY: number;
|
||||
readonly cursorX: number;
|
||||
readonly viewportY: number;
|
||||
readonly baseY: number;
|
||||
readonly length: number;
|
||||
getLine(y: number): IBufferLine | undefined;
|
||||
}
|
||||
|
||||
interface IBufferLine {
|
||||
readonly isWrapped: boolean;
|
||||
getCell(x: number): IBufferCell;
|
||||
translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
|
||||
}
|
||||
|
||||
interface IBufferCell {
|
||||
readonly char: string;
|
||||
readonly width: number;
|
||||
}
|
||||
|
||||
class TestSearchHelper extends SearchHelper {
|
||||
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
return this._findInLine(term, y, searchOptions);
|
||||
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
return this._findInLine(term, rowNumber, 0, searchOptions);
|
||||
}
|
||||
public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult {
|
||||
return this._findInLine(term, row, col, searchOptions, isReverseSearch);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +166,6 @@ describe('search addon', () => {
|
||||
it('should not select empty lines', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 3});
|
||||
term.core.write(' ');
|
||||
term.pushWriteData();
|
||||
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||
expect(line).eql(undefined);
|
||||
});
|
||||
@@ -247,5 +293,66 @@ describe('search addon', () => {
|
||||
expect(hello4).eql(undefined);
|
||||
expect(hello5).eql(undefined);
|
||||
});
|
||||
it('should find multiple matches in line', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('helloooo helloooo\r\naaaAAaaAAA');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: false,
|
||||
wholeWord: false,
|
||||
caseSensitive: false
|
||||
};
|
||||
const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions);
|
||||
const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions);
|
||||
const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions);
|
||||
const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions);
|
||||
const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions);
|
||||
expect(find0).eql({col: 0, row: 0, term: 'hello'});
|
||||
expect(find1).eql({col: 9, row: 0, term: 'hello'});
|
||||
expect(find2).eql({col: 0, row: 1, term: 'aaaa'});
|
||||
expect(find3).eql({col: 4, row: 1, term: 'aaaa'});
|
||||
expect(find4).eql(undefined);
|
||||
});
|
||||
it('should find multiple matches in line - reverse search', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('it is what it is');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: false,
|
||||
wholeWord: false,
|
||||
caseSensitive: false
|
||||
};
|
||||
const isReverseSearch = true;
|
||||
const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch);
|
||||
const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch);
|
||||
const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch);
|
||||
const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch);
|
||||
expect(find0).eql({col: 14, row: 0, term: 'is'});
|
||||
expect(find1).eql({col: 3, row: 0, term: 'is'});
|
||||
expect(find2).eql({col: 11, row: 0, term: 'it'});
|
||||
expect(find3).eql({col: 0, row: 0, term: 'it'});
|
||||
});
|
||||
it('should find multiple matches in line - reverse search with regex', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('zzzABCzzzzABCABC');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: true,
|
||||
wholeWord: false,
|
||||
caseSensitive: true
|
||||
};
|
||||
const isReverseSearch = true;
|
||||
const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch);
|
||||
const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch);
|
||||
const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch);
|
||||
const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch);
|
||||
expect(find0).eql({col: 13, row: 0, term: 'ABC'});
|
||||
expect(find1).eql({col: 10, row: 0, term: 'ABC'});
|
||||
expect(find2).eql({col: 3, row: 0, term: 'ABC'});
|
||||
expect(find3).eql(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
@@ -10,13 +11,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
|
||||
export interface ITerminadoAddonTerminal extends Terminal {
|
||||
_core: {
|
||||
register<T extends IDisposable>(d: T): void;
|
||||
};
|
||||
|
||||
__socket?: WebSocket;
|
||||
__attachSocketBuffer?: string;
|
||||
__dataListener?: IDisposable;
|
||||
|
||||
__getMessage?(ev: MessageEvent): void;
|
||||
__flushBuffer?(): void;
|
||||
|
||||
@@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
|
||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal.on('data', addonTerminal.__sendData);
|
||||
addonTerminal._core.register(addonTerminal.onData(addonTerminal.__sendData));
|
||||
}
|
||||
addonTerminal.on('resize', addonTerminal.__setSize);
|
||||
addonTerminal._core.register(addonTerminal.onResize(addonTerminal.__setSize));
|
||||
|
||||
socket.addEventListener('close', () => terminadoDetach(addonTerminal, socket));
|
||||
socket.addEventListener('error', () => terminadoDetach(addonTerminal, socket));
|
||||
@@ -75,7 +75,8 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
|
||||
*/
|
||||
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
|
||||
const addonTerminal = <ITerminadoAddonTerminal>term;
|
||||
addonTerminal.off('data', addonTerminal.__sendData);
|
||||
addonTerminal.__dataListener.dispose();
|
||||
addonTerminal.__dataListener = undefined;
|
||||
|
||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||
|
||||
|
||||
@@ -10,13 +10,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -11,13 +11,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -28,39 +28,185 @@ describe('webLinks addon', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow ~ character in URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
describe('should allow simple URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
const row = ' http://foo.com ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
assert.equal(uri, 'http://foo.com');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow : character in URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
describe('should allow ~ character in URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/colon:test ';
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/a~b#c~d?e~f ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/a~b#c~d?e~f');
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow : character at the end of a URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
describe('should allow : character in URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/colon:test: ';
|
||||
const row = ' http://foo.com/colon:test ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/colon:test ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/colon:test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not allow : character at the end of a URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/colon:test: ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/colon:test');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://bar.io/colon:test: ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/colon:test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not allow " character at the end of a URI enclosed with ""', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '"http://foo.com/"';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '"http://bar.io/"';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not allow \' character at the end of a URI enclosed with \'\'', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '\'http://foo.com/\'';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = '\'http://bar.io/\'';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should allow + character in URI path', () => {
|
||||
it('foo.com', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = 'http://foo.com/subpath/+/id';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/subpath/+/id');
|
||||
});
|
||||
|
||||
it('bar.io', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = 'http://bar.io/subpath/+/id';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://bar.io/subpath/+/id');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,8 @@ 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\\.\\-%~:]*)*([^:\\s])';
|
||||
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])';
|
||||
const pathClause = '(' + pathCharacterSet + ')?';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
}
|
||||
|
||||
export interface IWinptyCompatAddonTerminal extends Terminal {
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "xterm.winptycompat",
|
||||
"main": "winptyCompat.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/winptyCompat/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
|
||||
import * as winptyCompat from './winptyCompat';
|
||||
|
||||
class MockTerminal {}
|
||||
|
||||
describe('winptyCompat addon', () => {
|
||||
describe('apply', () => {
|
||||
it('should do register the `winptyCompatInit` method', () => {
|
||||
winptyCompat.apply(<any>MockTerminal);
|
||||
assert.equal(typeof (<any>MockTerminal).prototype.winptyCompatInit, 'function');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
const CHAR_DATA_CODE_INDEX = 3;
|
||||
const NULL_CELL_CODE = 32;
|
||||
|
||||
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._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||
const lastChar = line.get(addonTerminal.cols - 1);
|
||||
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
|
||||
winptyCompatInit(this);
|
||||
};
|
||||
}
|
||||
@@ -10,13 +10,12 @@
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../.."
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
+38
-24
@@ -3,22 +3,38 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ICircularList } from './Types';
|
||||
import { EventEmitter2, IEvent } from './EventEmitter2';
|
||||
|
||||
export interface IInsertEvent {
|
||||
index: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export interface IDeleteEvent {
|
||||
index: number;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||
* overriding values at the start of the list.
|
||||
*/
|
||||
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
export class CircularList<T> implements ICircularList<T> {
|
||||
protected _array: (T | undefined)[];
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
public onDeleteEmitter = new EventEmitter2<IDeleteEvent>();
|
||||
public get onDelete(): IEvent<IDeleteEvent> { return this.onDeleteEmitter.event; }
|
||||
public onInsertEmitter = new EventEmitter2<IInsertEvent>();
|
||||
public get onInsert(): IEvent<IInsertEvent> { return this.onInsertEmitter.event; }
|
||||
public onTrimEmitter = new EventEmitter2<number>();
|
||||
public get onTrim(): IEvent<number> { return this.onTrimEmitter.event; }
|
||||
|
||||
constructor(
|
||||
private _maxLength: number
|
||||
) {
|
||||
super();
|
||||
this._array = new Array<T>(this._maxLength);
|
||||
this._startIndex = 0;
|
||||
this._length = 0;
|
||||
@@ -91,7 +107,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._array[this._getCyclicIndex(this._length)] = value;
|
||||
if (this._length === this._maxLength) {
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.emit('trim', 1);
|
||||
this.onTrimEmitter.fire(1);
|
||||
} else {
|
||||
this._length++;
|
||||
}
|
||||
@@ -107,7 +123,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
throw new Error('Can only recycle when the buffer is full');
|
||||
}
|
||||
this._startIndex = ++this._startIndex % this._maxLength;
|
||||
this.emit('trim', 1);
|
||||
this.onTrimEmitter.fire(1);
|
||||
return this._array[this._getCyclicIndex(this._length - 1)]!;
|
||||
}
|
||||
|
||||
@@ -144,24 +160,22 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._length -= deleteCount;
|
||||
}
|
||||
|
||||
if (items && items.length) {
|
||||
// Add items
|
||||
for (let i = this._length - 1; i >= start; i--) {
|
||||
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
// Add items
|
||||
for (let i = this._length - 1; i >= start; i--) {
|
||||
this._array[this._getCyclicIndex(i + items.length)] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
this._array[this._getCyclicIndex(start + i)] = items[i];
|
||||
}
|
||||
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this._maxLength) {
|
||||
const countToTrim = (this._length + items.length) - this._maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this._maxLength;
|
||||
this.emit('trim', countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
// Adjust length as needed
|
||||
if (this._length + items.length > this._maxLength) {
|
||||
const countToTrim = (this._length + items.length) - this._maxLength;
|
||||
this._startIndex += countToTrim;
|
||||
this._length = this._maxLength;
|
||||
this.onTrimEmitter.fire(countToTrim);
|
||||
} else {
|
||||
this._length += items.length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,7 +189,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
}
|
||||
this._startIndex += count;
|
||||
this._length -= count;
|
||||
this.emit('trim', count);
|
||||
this.onTrimEmitter.fire(count);
|
||||
}
|
||||
|
||||
public shiftElements(start: number, count: number, offset: number): void {
|
||||
@@ -199,7 +213,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
while (this._length > this._maxLength) {
|
||||
this._length--;
|
||||
this._startIndex++;
|
||||
this.emit('trim', 1);
|
||||
this.onTrimEmitter.fire(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -101,7 +101,7 @@ describe('clone', () => {
|
||||
test.a.b.c.d.e.f = 'bar';
|
||||
|
||||
// The values at a greater depth then 5 should not be cloned
|
||||
assert.equal(cloned.a.b.c.d.e.f, 'bar');
|
||||
assert.equal((cloned as any).a.b.c.d.e.f, 'bar');
|
||||
});
|
||||
|
||||
it('should allow an optional maximum depth to be set', () => {
|
||||
@@ -118,7 +118,7 @@ describe('clone', () => {
|
||||
test.a.b.c = 'bar';
|
||||
|
||||
// The values at a greater depth then 2 should not be cloned
|
||||
assert.equal(cloned.a.b.c, 'bar');
|
||||
assert.equal((cloned as any).a.b.c, 'bar');
|
||||
});
|
||||
|
||||
it('should not throw when cloning a recursive reference', () => {
|
||||
@@ -6,7 +6,7 @@
|
||||
/*
|
||||
* A simple utility for cloning values
|
||||
*/
|
||||
export const clone = <T>(val: T, depth: number = 5): T => {
|
||||
export function clone<T>(val: T, depth: number = 5): T | null {
|
||||
if (typeof val !== 'object') {
|
||||
return val;
|
||||
}
|
||||
@@ -25,4 +25,4 @@ export const clone = <T>(val: T, depth: number = 5): T => {
|
||||
}
|
||||
|
||||
return clonedObject as T;
|
||||
};
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
import { IDisposable, IEventEmitter, XtermListener } from './Types';
|
||||
import { Disposable } from './Lifecycle';
|
||||
|
||||
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
|
||||
@@ -23,7 +22,7 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a disposabe listener to the EventEmitter, returning the disposable.
|
||||
* Adds a disposable listener to the EventEmitter, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
@@ -75,6 +74,19 @@ export class EventEmitter extends Disposable implements IEventEmitter, IDisposab
|
||||
}
|
||||
}
|
||||
|
||||
public emitMayRemoveListeners(type: string, ...args: any[]): void {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
const obj = this._events[type];
|
||||
let length = obj.length;
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
obj[i].apply(this, args);
|
||||
i -= length - obj.length;
|
||||
length = obj.length;
|
||||
}
|
||||
}
|
||||
|
||||
public listeners(type: string): XtermListener[] {
|
||||
return this._events[type] || [];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { EventEmitter2 } from './EventEmitter2';
|
||||
|
||||
describe('EventEmitter2', () => {
|
||||
it('should fire listeners multiple times', () => {
|
||||
const order: string[] = [];
|
||||
const emitter = new EventEmitter2<number>();
|
||||
emitter.event(data => order.push(data + 'a'));
|
||||
emitter.event(data => order.push(data + 'b'));
|
||||
emitter.fire(1);
|
||||
emitter.fire(2);
|
||||
assert.deepEqual(order, [ '1a', '1b', '2a', '2b' ]);
|
||||
});
|
||||
|
||||
it('should not fire listeners once disposed', () => {
|
||||
const order: string[] = [];
|
||||
const emitter = new EventEmitter2<number>();
|
||||
emitter.event(data => order.push(data + 'a'));
|
||||
const disposeB = emitter.event(data => order.push(data + 'b'));
|
||||
emitter.event(data => order.push(data + 'c'));
|
||||
emitter.fire(1);
|
||||
disposeB.dispose();
|
||||
emitter.fire(2);
|
||||
assert.deepEqual(order, [ '1a', '1b', '1c', '2a', '2c' ]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from './Types';
|
||||
|
||||
interface IListener<T> {
|
||||
(e: T): void;
|
||||
}
|
||||
|
||||
export interface IEvent<T> {
|
||||
(listener: (e: T) => any): IDisposable;
|
||||
}
|
||||
|
||||
export class EventEmitter2<T> {
|
||||
private _listeners: IListener<T>[] = [];
|
||||
private _event?: IEvent<T>;
|
||||
|
||||
public get event(): IEvent<T> {
|
||||
if (!this._event) {
|
||||
this._event = (listener: (e: T) => any) => {
|
||||
this._listeners.push(listener);
|
||||
const disposable = {
|
||||
dispose: () => {
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
if (this._listeners[i] === listener) {
|
||||
this._listeners.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return disposable;
|
||||
};
|
||||
}
|
||||
return this._event;
|
||||
}
|
||||
|
||||
public fire(data: T): void {
|
||||
const queue: IListener<T>[] = [];
|
||||
for (let i = 0; i < this._listeners.length; i++) {
|
||||
queue.push(this._listeners[i]);
|
||||
}
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
queue[i].call(undefined, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
import { IDisposable } from './Types';
|
||||
|
||||
/**
|
||||
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
|
||||
|
||||
@@ -3,6 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface INavigator {
|
||||
userAgent: string;
|
||||
language: string;
|
||||
platform: string;
|
||||
}
|
||||
|
||||
// We're declaring a navigator global here as we expect it in all runtimes (node and browser), but
|
||||
// we want this module to live in common.
|
||||
declare const navigator: INavigator;
|
||||
|
||||
const isNode = (typeof navigator === 'undefined') ? true : false;
|
||||
const userAgent = (isNode) ? 'node' : navigator.userAgent;
|
||||
const platform = (isNode) ? 'node' : navigator.platform;
|
||||
@@ -3,21 +3,20 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { fillFallback } from './TypedArrayUtils';
|
||||
import { fillFallback, concat } from './TypedArrayUtils';
|
||||
|
||||
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
describe('polyfill conformance tests', function(): void {
|
||||
|
||||
function deepEquals(a: TypedArray, b: TypedArray): void {
|
||||
assert.equal(a.length, b.length);
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
assert.equal(a[i], b[i]);
|
||||
}
|
||||
function deepEquals(a: TypedArray, b: TypedArray): void {
|
||||
assert.equal(a.length, b.length);
|
||||
for (let i = 0; i < a.length; ++i) {
|
||||
assert.equal(a[i], b[i]);
|
||||
}
|
||||
}
|
||||
|
||||
describe('polyfill conformance tests', function(): void {
|
||||
describe('TypedArray.fill', function(): void {
|
||||
it('should work with all typed array types', function(): void {
|
||||
const u81 = new Uint8Array(5);
|
||||
@@ -87,3 +86,12 @@ describe('polyfill conformance tests', function(): void {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('typed array convenience functions', () => {
|
||||
it('concat', () => {
|
||||
const a = new Uint8Array([1, 2, 3, 4, 5]);
|
||||
const b = new Uint8Array([6, 7, 8, 9, 0]);
|
||||
const merged = concat(a, b);
|
||||
deepEquals(merged, new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
|
||||
/**
|
||||
* polyfill for TypedArray.fill
|
||||
* This is needed to support .fill in all safari versions and IE 11.
|
||||
*/
|
||||
|
||||
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
||||
| Int8Array | Int16Array | Int32Array
|
||||
| Float32Array | Float64Array;
|
||||
|
||||
export function fill<T extends TypedArray>(array: T, value: number, start?: number, end?: number): T {
|
||||
// all modern engines that support .fill
|
||||
if (array.fill) {
|
||||
@@ -39,3 +39,14 @@ export function fillFallback<T extends TypedArray>(array: T, value: number, star
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Concat two typed arrays `a` and `b`.
|
||||
* Returns a new typed array.
|
||||
*/
|
||||
export function concat<T extends TypedArray>(a: T, b: T): T {
|
||||
const result = new (a.constructor as any)(a.length + b.length);
|
||||
result.set(a);
|
||||
result.set(b, a.length);
|
||||
return result;
|
||||
}
|
||||
|
||||
+23
-2
@@ -3,7 +3,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEventEmitter } from 'xterm';
|
||||
import { IEvent, EventEmitter2 } from './EventEmitter2';
|
||||
import { IDeleteEvent, IInsertEvent } from './CircularList';
|
||||
|
||||
export const DEFAULT_COLOR = 256;
|
||||
|
||||
export interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface IEventEmitter {
|
||||
on(type: string, listener: (...args: any[]) => void): void;
|
||||
off(type: string, listener: (...args: any[]) => void): void;
|
||||
emit(type: string, data?: any): void;
|
||||
addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable;
|
||||
}
|
||||
|
||||
export type XtermListener = (...args: any[]) => void;
|
||||
|
||||
@@ -21,11 +35,18 @@ export interface IKeyboardEvent {
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
export interface ICircularList<T> {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
isFull: boolean;
|
||||
|
||||
onDeleteEmitter: EventEmitter2<IDeleteEvent>;
|
||||
onDelete: IEvent<IDeleteEvent>;
|
||||
onInsertEmitter: EventEmitter2<IInsertEvent>;
|
||||
onInsert: IEvent<IInsertEvent>;
|
||||
onTrimEmitter: EventEmitter2<number>;
|
||||
onTrim: IEvent<number>;
|
||||
|
||||
get(index: number): T | undefined;
|
||||
set(index: number, value: T): void;
|
||||
push(value: T): void;
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig-library-base",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es5"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"pretty": true,
|
||||
"outDir": "../../lib",
|
||||
"types": [
|
||||
"../../node_modules/@types/mocha",
|
||||
"../../"
|
||||
"../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*"
|
||||
]
|
||||
"include": [ "./**/*" ]
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from '../common/Types';
|
||||
|
||||
export const enum KeyboardResultType {
|
||||
SEND_KEY,
|
||||
SELECT_ALL,
|
||||
@@ -19,3 +21,88 @@ export interface IKeyboardResult {
|
||||
export interface ICharset {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type IColorRGB = [number, number, number];
|
||||
|
||||
/** Attribute data */
|
||||
export interface IAttributeData {
|
||||
fg: number;
|
||||
bg: number;
|
||||
|
||||
clone(): IAttributeData;
|
||||
|
||||
// flags
|
||||
isInverse(): number;
|
||||
isBold(): number;
|
||||
isUnderline(): number;
|
||||
isBlink(): number;
|
||||
isInvisible(): number;
|
||||
isItalic(): number;
|
||||
isDim(): number;
|
||||
|
||||
// color modes
|
||||
getFgColorMode(): number;
|
||||
getBgColorMode(): number;
|
||||
isFgRGB(): boolean;
|
||||
isBgRGB(): boolean;
|
||||
isFgPalette(): boolean;
|
||||
isBgPalette(): boolean;
|
||||
isFgDefault(): boolean;
|
||||
isBgDefault(): boolean;
|
||||
|
||||
// colors
|
||||
getFgColor(): number;
|
||||
getBgColor(): number;
|
||||
}
|
||||
|
||||
/** Cell data */
|
||||
export interface ICellData extends IAttributeData {
|
||||
content: number;
|
||||
combinedData: string;
|
||||
isCombined(): number;
|
||||
getWidth(): number;
|
||||
getChars(): string;
|
||||
getCode(): number;
|
||||
setFromCharData(value: CharData): void;
|
||||
getAsCharData(): CharData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a line in the terminal buffer.
|
||||
*/
|
||||
export interface IBufferLine {
|
||||
length: number;
|
||||
isWrapped: boolean;
|
||||
get(index: number): CharData;
|
||||
set(index: number, value: CharData): void;
|
||||
loadCell(index: number, cell: ICellData): ICellData;
|
||||
setCell(index: number, cell: ICellData): void;
|
||||
setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
|
||||
addCodepointToCell(index: number, codePoint: number): void;
|
||||
insertCells(pos: number, n: number, ch: ICellData): void;
|
||||
deleteCells(pos: number, n: number, fill: ICellData): void;
|
||||
replaceCells(start: number, end: number, fill: ICellData): void;
|
||||
resize(cols: number, fill: ICellData): void;
|
||||
fill(fillCellData: ICellData): void;
|
||||
copyFrom(line: IBufferLine): void;
|
||||
clone(): IBufferLine;
|
||||
getTrimmedLength(): number;
|
||||
translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
|
||||
|
||||
/* direct access to cell attrs */
|
||||
getWidth(index: number): number;
|
||||
hasWidth(index: number): number;
|
||||
getFg(index: number): number;
|
||||
getBg(index: number): number;
|
||||
hasContent(index: number): number;
|
||||
getCodePoint(index: number): number;
|
||||
isCombined(index: number): number;
|
||||
getString(index: number): string;
|
||||
}
|
||||
|
||||
export interface IMarker extends IDisposable {
|
||||
readonly id: number;
|
||||
readonly isDisposed: boolean;
|
||||
readonly line: number;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import { BufferLine, CellData, Content, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './BufferLine';
|
||||
import { CharData, IBufferLine } from '../Types';
|
||||
|
||||
class TestBufferLine extends BufferLine {
|
||||
public get combined(): {[index: number]: string} {
|
||||
return this._combined;
|
||||
}
|
||||
|
||||
public toArray(): CharData[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < this.length; ++i) {
|
||||
result.push(this.loadCell(i, new CellData()).getAsCharData());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
describe('CellData', () => {
|
||||
it('CharData <--> CellData equality', () => {
|
||||
const cell = new CellData();
|
||||
// ASCII
|
||||
cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
// combining
|
||||
cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
// surrogate
|
||||
cell.setFromCharData([123, '𝄞', 1, 0x1D11E]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
// surrogate + combining
|
||||
cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
// wide char
|
||||
cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]);
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]);
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BufferLine', function(): void {
|
||||
it('ctor', function(): void {
|
||||
let line: IBufferLine = new TestBufferLine(0);
|
||||
chai.expect(line.length).equals(0);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10, undefined, true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
});
|
||||
it('insertCells', function(): void {
|
||||
const line = new TestBufferLine(3);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('deleteCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[4, 'd', 0, 'd'.charCodeAt(0)],
|
||||
[5, 'e', 0, 'e'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('replaceCells', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql([
|
||||
[1, 'a', 0, 'a'.charCodeAt(0)],
|
||||
[2, 'b', 0, 'b'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[6, 'f', 0, 'f'.charCodeAt(0)],
|
||||
[5, 'e', 0, 'e'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('fill', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql([
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)],
|
||||
[123, 'z', 0, 'z'.charCodeAt(0)]
|
||||
]);
|
||||
});
|
||||
it('clone', function(): void {
|
||||
const line = new TestBufferLine(5, undefined, true);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
const line2 = line.clone();
|
||||
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
chai.expect(line2.isWrapped).equals(line.isWrapped);
|
||||
});
|
||||
it('copyFrom', function(): void {
|
||||
const line = new TestBufferLine(5);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
chai.expect(line2.length).equals(line.length);
|
||||
chai.expect(line2.isWrapped).equals(line.isWrapped);
|
||||
});
|
||||
it('should support combining chars', function(): void {
|
||||
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
|
||||
// --> set code to the last charCodeAt value of the string
|
||||
// Note: needs to be fixed once the string pointer is in place
|
||||
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
|
||||
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
|
||||
line2.copyFrom(line);
|
||||
chai.expect(line2.toArray()).eql(line.toArray());
|
||||
const line3 = line.clone();
|
||||
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
|
||||
});
|
||||
describe('resize', function(): void {
|
||||
it('enlarge(false)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('enlarge(true)', function(): void {
|
||||
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink(true) - should apply new size', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('shrink to 0 length', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
});
|
||||
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
|
||||
line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
|
||||
chai.expect(Object.keys(line.combined).length).eql(2);
|
||||
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString()).eql('aa😁aa');
|
||||
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
|
||||
chai.expect(Object.keys(line.combined).length).eql(1);
|
||||
});
|
||||
});
|
||||
describe('getTrimLength', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
chai.expect(line.getTrimmedLength()).equal(0);
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
chai.expect(line.getTrimmedLength()).equal(3);
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
|
||||
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
|
||||
});
|
||||
});
|
||||
describe('translateToString with and w\'o trimming', function(): void {
|
||||
it('empty line', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
chai.expect(line.translateToString(false)).equal(' ');
|
||||
chai.expect(line.translateToString(true)).equal('');
|
||||
});
|
||||
it('ASCII', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
|
||||
|
||||
});
|
||||
it('surrogate', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
|
||||
});
|
||||
it('combining', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
|
||||
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
|
||||
});
|
||||
it('fullwidth', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(6, CellData.fromCharData([0, '', 0, 0]));
|
||||
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
|
||||
line.setCell(8, CellData.fromCharData([0, '', 0, 0]));
|
||||
chai.expect(line.translateToString(false)).equal('a 1 11 ');
|
||||
chai.expect(line.translateToString(true)).equal('a 1 11');
|
||||
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
|
||||
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
|
||||
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
|
||||
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
|
||||
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
|
||||
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
|
||||
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
|
||||
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
|
||||
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
|
||||
});
|
||||
it('space at end', function(): void {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(2, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString(false)).equal('a a aa ');
|
||||
chai.expect(line.translateToString(true)).equal('a a aa ');
|
||||
});
|
||||
it('should always return some sane value', function(): void {
|
||||
// sanity check - broken line with invalid out of bound null width cells
|
||||
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
|
||||
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
chai.expect(line.translateToString(false)).equal(' ');
|
||||
chai.expect(line.translateToString(true)).equal('');
|
||||
});
|
||||
it('should work with endCol=0', () => {
|
||||
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
|
||||
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
chai.expect(line.translateToString(true, 0, 0)).equal('');
|
||||
});
|
||||
});
|
||||
describe('addCharToCell', () => {
|
||||
it('should set width to 1 for empty cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
const cell = line.loadCell(0, new CellData());
|
||||
// chars contains single combining char
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), 0);
|
||||
});
|
||||
it('should add char to combining string in cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const cell = line .loadCell(0, new CellData());
|
||||
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
|
||||
line.setCell(0, cell);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
line.loadCell(0, cell);
|
||||
// chars contains 3 chars
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
});
|
||||
it('should create combining string on taken cell', () => {
|
||||
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
|
||||
const cell = line .loadCell(0, new CellData());
|
||||
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
|
||||
line.setCell(0, cell);
|
||||
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
|
||||
line.loadCell(0, cell);
|
||||
// chars contains 2 chars
|
||||
// width is set to 1
|
||||
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]);
|
||||
// do not account a single combining char as combined
|
||||
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
|
||||
});
|
||||
});
|
||||
});
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user