Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
1c62980d20 | ||
|
|
2d8f420db8 | ||
|
|
2cb293ceb2 | ||
|
|
3d2ae2b01e | ||
|
|
1c4a71f0df | ||
|
|
30a667c3be | ||
|
|
03115639a9 | ||
|
|
48629e7eb2 | ||
|
|
6b9fb48898 | ||
|
|
fe9eb7d31e | ||
|
|
693e17a5cb | ||
|
|
b5b2bcd0c9 | ||
|
|
fd34caee23 | ||
|
|
278d696709 | ||
|
|
7f18199985 | ||
|
|
b68974517e | ||
|
|
4d660deff3 | ||
|
|
b5f1c3384a | ||
|
|
6f4a6ef308 | ||
|
|
18902dd789 | ||
|
|
66ec57b431 | ||
|
|
6ee9178302 | ||
|
|
6e45d022ac | ||
|
|
4d270bf72c | ||
|
|
38adfcd81c | ||
|
|
376c790e1d | ||
|
|
5f989929c3 | ||
|
|
9d4beb024a | ||
|
|
99e4a9649f | ||
|
|
eb27641928 | ||
|
|
2483a98f63 | ||
|
|
5d3ef759b4 | ||
|
|
4beb3a9ecd | ||
|
|
9df56aba13 | ||
|
|
8cf98ec546 | ||
|
|
a6ef64dcb6 | ||
|
|
a4e3646640 | ||
|
|
591dfd506d | ||
|
|
68d5ce947e | ||
|
|
13175976d0 | ||
|
|
f4a522516c | ||
|
|
1db17701df | ||
|
|
c6f1de7970 | ||
|
|
14d6960873 | ||
|
|
5fca73aa5a | ||
|
|
06e89549a8 | ||
|
|
929d722e0e | ||
|
|
79a1194bea | ||
|
|
8a04431417 | ||
|
|
76e7057c94 | ||
|
|
62b1aa5a67 | ||
|
|
41ba20f749 | ||
|
|
32574a1a09 | ||
|
|
fc6f101e16 | ||
|
|
2e889852b0 | ||
|
|
1b5ef5d93a | ||
|
|
4f18717940 | ||
|
|
266ea38d91 | ||
|
|
c6c67587e1 | ||
|
|
f363872f1e | ||
|
|
478dfee6e2 | ||
|
|
d9513ce50d | ||
|
|
a693eb8e9b | ||
|
|
fe027a2e9d | ||
|
|
65c5df92d9 | ||
|
|
dc0ae4a815 | ||
|
|
66272eb310 | ||
|
|
913adf9ccc | ||
|
|
b4faaef785 | ||
|
|
2f453f968c | ||
|
|
dc077a181e | ||
|
|
692cdfa390 | ||
|
|
6b89ec9d8e | ||
|
|
53fe096455 | ||
|
|
549c28bfa4 | ||
|
|
2687fd02b2 | ||
|
|
1ceecf3f14 | ||
|
|
98a3892e52 | ||
|
|
8c33de395d | ||
|
|
ef6fb93a92 | ||
|
|
36db03e713 | ||
|
|
895d6fc479 | ||
|
|
15c4cc8a74 | ||
|
|
3964bb28b5 | ||
|
|
006869704b | ||
|
|
21f848d0ab | ||
|
|
9b762854eb | ||
|
|
e4a84643b8 | ||
|
|
02732d9272 | ||
|
|
9286ea3eaf | ||
|
|
203ee0f644 | ||
|
|
579ef28929 | ||
|
|
bcd3a4dc40 | ||
|
|
a480f483cd | ||
|
|
2f90035243 | ||
|
|
a19d93851c | ||
|
|
a5894ee234 | ||
|
|
3ca0598279 | ||
|
|
3d2035e1a0 | ||
|
|
ef6f0db0a0 | ||
|
|
0e3f9b72d1 | ||
|
|
e3146961be | ||
|
|
9faebaf5fe | ||
|
|
19174dd836 | ||
|
|
e4e5b966da | ||
|
|
8ea0bb55b4 | ||
|
|
cb97ab35cb | ||
|
|
774793c7ca | ||
|
|
4aa822e7a9 | ||
|
|
de7d5e1eec | ||
|
|
3e233be54b | ||
|
|
70981c7a29 | ||
|
|
8acf137d14 | ||
|
|
a15b866013 | ||
|
|
2f9c04c3af | ||
|
|
67df9a5cdc | ||
|
|
22e4a2dc97 | ||
|
|
e9906f9b9e | ||
|
|
eca1170a45 | ||
|
|
5395503031 | ||
|
|
200b59b47c | ||
|
|
cabd4a721c | ||
|
|
745e228e98 | ||
|
|
8ffcd65cff | ||
|
|
1c111d3e60 | ||
|
|
62f1d64af4 | ||
|
|
ee6fa6ca00 | ||
|
|
5173bd6419 | ||
|
|
160b4a7d66 | ||
|
|
fb64c527a5 | ||
|
|
aaffc2b2be | ||
|
|
c7eb727220 | ||
|
|
f3cd652e74 | ||
|
|
108438dbeb | ||
|
|
41b1c929e0 | ||
|
|
793516060f | ||
|
|
60468356db | ||
|
|
e90f8611d5 | ||
|
|
93d7a3021d | ||
|
|
982006836b | ||
|
|
d137880876 | ||
|
|
fc8640af1e | ||
|
|
213dadf1bd | ||
|
|
2efd28ad4b | ||
|
|
ba8d341416 | ||
|
|
16614b7179 | ||
|
|
c77af88010 | ||
|
|
50bc11b28b | ||
|
|
565f46075c | ||
|
|
c2994b0ae1 | ||
|
|
4d556fcaf0 | ||
|
|
083bc0f3d1 | ||
|
|
1208fc9e10 | ||
|
|
1585aba5b5 | ||
|
|
82228ad891 | ||
|
|
6f6c7c20c0 | ||
|
|
b9ab35dfe9 | ||
|
|
35db216f64 | ||
|
|
00c1211435 | ||
|
|
8bd49cba35 | ||
|
|
4a2189d140 | ||
|
|
f2fb46cc82 | ||
|
|
74664a9161 | ||
|
|
5bc7fc4256 | ||
|
|
af0ca6c2cb | ||
|
|
3f3263cbb0 | ||
|
|
161dfa8d2d | ||
|
|
8a6ed52e4e | ||
|
|
3edd2f2816 | ||
|
|
33ee08da47 | ||
|
|
a53aaa5f25 | ||
|
|
f61ba9558b | ||
|
|
c6eb56670d | ||
|
|
19f6c0a940 | ||
|
|
37972aa5dd | ||
|
|
a0250c5cea | ||
|
|
9e1551e2f1 | ||
|
|
d25e55ff45 | ||
|
|
b650db5b1a | ||
|
|
4450dc9206 | ||
|
|
69e6d7d9cc | ||
|
|
e66a10889f | ||
|
|
375b5c5b4f | ||
|
|
2eec8f951f | ||
|
|
64d6354983 | ||
|
|
8843b0cbc6 | ||
|
|
588ed5e3af | ||
|
|
05b68ee4ae | ||
|
|
25e5795374 | ||
|
|
eb123ca787 | ||
|
|
58003d4e29 | ||
|
|
c12aec3484 | ||
|
|
f2ecd06e64 | ||
|
|
751e2efa75 | ||
|
|
c739d2fe87 | ||
|
|
45e64a9167 | ||
|
|
a63779ae7b | ||
|
|
6e350219aa | ||
|
|
33a358033c | ||
|
|
0065682bd7 | ||
|
|
18035e239e | ||
|
|
91d04f2552 | ||
|
|
bc16a8b7e7 | ||
|
|
1e9564788e | ||
|
|
791c020a56 | ||
|
|
febc3ba2b8 | ||
|
|
1c5dfb5f34 | ||
|
|
3877e96d7f | ||
|
|
f2c7de8d53 | ||
|
|
ed5467689a | ||
|
|
8d16bb6ea2 | ||
|
|
2febd9b248 | ||
|
|
5b7aa68299 | ||
|
|
3dbc6e604f | ||
|
|
211cca8fda | ||
|
|
e7ae4be394 | ||
|
|
b85b9bb3c3 | ||
|
|
77da89f5da | ||
|
|
f5038bea3d | ||
|
|
ae61292b10 | ||
|
|
8fd4bdd678 | ||
|
|
92b5c902de | ||
|
|
a7b051c52f | ||
|
|
b92bbf578b | ||
|
|
18871f0d3c | ||
|
|
efe007b89b | ||
|
|
da027b2e11 | ||
|
|
d4ec50e7de | ||
|
|
5a1c71c1d5 | ||
|
|
bbf36fe091 | ||
|
|
990bf5c973 | ||
|
|
b28bba56df | ||
|
|
21f3534668 | ||
|
|
fefc6c6a34 | ||
|
|
00bf16f610 | ||
|
|
57a6fe5b87 | ||
|
|
9e446a9a0e | ||
|
|
16c0ad858a | ||
|
|
750ec3870c | ||
|
|
e9d54bf011 | ||
|
|
408403e2a0 | ||
|
|
a30bb22c7a | ||
|
|
a531b4dd9b | ||
|
|
4cd591cb21 | ||
|
|
10687b138d | ||
|
|
1108d0746e | ||
|
|
005785b84d | ||
|
|
2e8e64eb4d | ||
|
|
f44d57b2e8 | ||
|
|
36c53a5f8c | ||
|
|
995e384980 | ||
|
|
4941d5a19e | ||
|
|
1f2d223e64 | ||
|
|
d1991396e7 | ||
|
|
478d824fa7 | ||
|
|
39a7b18065 | ||
|
|
2c62c3a8c3 | ||
|
|
4463f8d810 | ||
|
|
c05b6bd885 | ||
|
|
290c95ad71 | ||
|
|
57bbc817bb | ||
|
|
fb0b73ab83 | ||
|
|
d1deca2330 | ||
|
|
33915f3ae0 | ||
|
|
e736f1223f | ||
|
|
a7a58aa055 | ||
|
|
cbe7b21090 | ||
|
|
d9ec47ab7a | ||
|
|
94b20d40bf | ||
|
|
202734f10c | ||
|
|
4656bd433c | ||
|
|
f6d5abf7b3 | ||
|
|
6ea56cd85e | ||
|
|
4d7ce4da46 | ||
|
|
39047767e4 | ||
|
|
8d4b5cdc55 | ||
|
|
00af63b271 | ||
|
|
608d78b6ac | ||
|
|
ac3595e8b4 | ||
|
|
5284a5a742 | ||
|
|
842ee6c164 | ||
|
|
d0dc387921 | ||
|
|
7762ab96a5 | ||
|
|
84e9ad2adc | ||
|
|
6528ba84ff | ||
|
|
0bcf30a21b | ||
|
|
86c78e726d | ||
|
|
cf9cb80cb4 | ||
|
|
04f2d52f26 | ||
|
|
192f751862 | ||
|
|
de143b552d | ||
|
|
d37a8ce539 | ||
|
|
2f66ec452c | ||
|
|
4b68976dfb | ||
|
|
3143ad7f02 | ||
|
|
631e331077 | ||
|
|
70be55df75 | ||
|
|
14c20c5f32 | ||
|
|
57bbbda7e8 | ||
|
|
8664f54a76 | ||
|
|
9514bfd31c | ||
|
|
9ce2894353 | ||
|
|
5c873c1ce2 | ||
|
|
4d4d9914a8 | ||
|
|
a19673d835 | ||
|
|
f7dbadc79b | ||
|
|
50d600877c | ||
|
|
cd2ac929c3 | ||
|
|
7401567856 | ||
|
|
69ab27004e | ||
|
|
65a528c18b | ||
|
|
a8d9c92ae1 | ||
|
|
bd68853575 | ||
|
|
69a6c04620 | ||
|
|
68766d540f | ||
|
|
a09e6a6238 | ||
|
|
30f74bf5f3 | ||
|
|
2708a76501 | ||
|
|
c8c22e3ddf | ||
|
|
7fc1fd947f | ||
|
|
7e69c5d0db | ||
|
|
0068c477b9 | ||
|
|
74474da7f3 | ||
|
|
e6e079bfa1 | ||
|
|
7dc8118367 | ||
|
|
5aa6656971 | ||
|
|
8979eea810 | ||
|
|
7d2bdacf77 | ||
|
|
1edc732c55 | ||
|
|
e03c93fb81 | ||
|
|
d23c65d1db | ||
|
|
07b5bd0b64 | ||
|
|
35483b3bf4 | ||
|
|
6ebf17bf04 | ||
|
|
5d37a17383 | ||
|
|
dade9ba050 | ||
|
|
5909885d43 | ||
|
|
0446cf3aee | ||
|
|
3a97dbecde | ||
|
|
c88bc62ff9 | ||
|
|
a2bfbeaa74 | ||
|
|
ade3e0943c | ||
|
|
16de86f5ea | ||
|
|
fed579238a | ||
|
|
11426bb0d0 | ||
|
|
5e004b21a3 | ||
|
|
b2ba281916 | ||
|
|
bcf223e235 | ||
|
|
d5c17a0cc9 | ||
|
|
54ed988fd1 | ||
|
|
1460a34796 | ||
|
|
bc156294aa | ||
|
|
09028d4d86 | ||
|
|
c35306aaa6 | ||
|
|
3289eed2a2 | ||
|
|
a76d4a6055 | ||
|
|
12e38404a2 | ||
|
|
08af72be1a | ||
|
|
daa6fa0a66 | ||
|
|
4aa8e402a4 | ||
|
|
a494e80a9f | ||
|
|
a8fe4107a3 | ||
|
|
6c2acd6e34 | ||
|
|
f2049694c8 | ||
|
|
e59fef87cc | ||
|
|
d943a5c890 | ||
|
|
715c078b6d | ||
|
|
7447a8d08d | ||
|
|
f9717bc635 | ||
|
|
7ef18cca1e | ||
|
|
c291ab1058 | ||
|
|
d4d1426bff | ||
|
|
25e0e97ef2 | ||
|
|
0f28ccb093 | ||
|
|
f4bb4a22d4 | ||
|
|
a754f37209 | ||
|
|
d4f7c6bcf5 | ||
|
|
4eede85ef6 | ||
|
|
bc943b7b57 | ||
|
|
bdb6647ae0 | ||
|
|
0d89fcdeeb | ||
|
|
4ffc076f84 | ||
|
|
2ed3997aaa | ||
|
|
a0f9768fe8 | ||
|
|
8b334b01a6 | ||
|
|
c6029765a4 | ||
|
|
520dacac27 | ||
|
|
dcc1b7a3e3 | ||
|
|
27c3663470 | ||
|
|
599271bde4 | ||
|
|
2716413e66 | ||
|
|
e5465841f8 | ||
|
|
03e54cce7d | ||
|
|
88205511e6 | ||
|
|
374a8b70eb | ||
|
|
c8b8e39dde | ||
|
|
48cf73c152 | ||
|
|
1dc88d2e29 | ||
|
|
7873598a41 | ||
|
|
7c1b1e2eb8 | ||
|
|
2877c0da80 | ||
|
|
fb72f25067 | ||
|
|
3618c6495b | ||
|
|
443b2dbc28 | ||
|
|
53e995a2d8 | ||
|
|
53193d0a47 | ||
|
|
165c8d4522 | ||
|
|
92341c3e7b | ||
|
|
02672dc22e | ||
|
|
57b8e064a8 | ||
|
|
3a310f1597 | ||
|
|
4d0217f816 | ||
|
|
559e932a2e | ||
|
|
aa427010e1 | ||
|
|
1aec57e8c0 | ||
|
|
cbbf6bd654 | ||
|
|
33b65bbfaa | ||
|
|
8e39650b5f | ||
|
|
f148238117 | ||
|
|
6057586ff3 | ||
|
|
e2267fce92 | ||
|
|
784a142dac | ||
|
|
71aa7027bd | ||
|
|
ca76ebc396 | ||
|
|
54187b3597 | ||
|
|
d58f5cac01 | ||
|
|
b3e8980270 | ||
|
|
4d14e00034 | ||
|
|
e75578440e | ||
|
|
70afb13c9b | ||
|
|
6a2c4b9823 | ||
|
|
e2cfd94005 | ||
|
|
9fe8ea8de2 | ||
|
|
46311b87a2 | ||
|
|
692ca1c9a3 | ||
|
|
bf5c1f0f78 | ||
|
|
467a93c8d9 | ||
|
|
3841edadb5 | ||
|
|
76edd8a3bb | ||
|
|
284b083274 | ||
|
|
f9453ef4fa | ||
|
|
024404f548 | ||
|
|
dc332480cc | ||
|
|
a194263183 | ||
|
|
f3561fad30 | ||
|
|
3f2605f35c | ||
|
|
d1f6798e75 | ||
|
|
841a33be28 | ||
|
|
dd7081f948 | ||
|
|
999fce4e52 | ||
|
|
0d49d1f64d | ||
|
|
11df1e1d80 | ||
|
|
a74ca25b09 | ||
|
|
32231dcd41 | ||
|
|
04a847f0e2 | ||
|
|
276e65fa0b | ||
|
|
a1421b151e | ||
|
|
71f5965f8e | ||
|
|
59fc4af9a0 | ||
|
|
381e6c1740 | ||
|
|
9a54cf639b | ||
|
|
01a4ec21da | ||
|
|
a65bd1a77c | ||
|
|
6439f6e856 | ||
|
|
d24caf14a4 | ||
|
|
52f132a570 | ||
|
|
424d3257d1 | ||
|
|
f47f095b89 | ||
|
|
de26be56c1 | ||
|
|
f964a34b34 | ||
|
|
6576d513b0 | ||
|
|
4af2d1ed41 | ||
|
|
9e27c231e5 | ||
|
|
ae3684c897 | ||
|
|
d4c621a182 | ||
|
|
9c68eb3b11 | ||
|
|
77cb697990 | ||
|
|
cee432a66a | ||
|
|
a553415216 | ||
|
|
cd8477a942 | ||
|
|
811b3cc326 | ||
|
|
a99cac45f7 | ||
|
|
3d34881c4e | ||
|
|
0340d91150 | ||
|
|
5c3315251b | ||
|
|
e769e787a8 | ||
|
|
96ed32698b | ||
|
|
e66928da20 | ||
|
|
c762885c4b | ||
|
|
63ced6f302 | ||
|
|
82f6547f10 | ||
|
|
841a0e010d | ||
|
|
85f25e77f5 | ||
|
|
2adaed87d1 | ||
|
|
4c93d8ddaa | ||
|
|
b35fa9576e | ||
|
|
091c42f7e6 | ||
|
|
6601133912 | ||
|
|
9273e8175e | ||
|
|
cd75d6fb68 | ||
|
|
01bfa1df4b | ||
|
|
123e074c16 | ||
|
|
929150a8e1 | ||
|
|
e3eb1b81b3 | ||
|
|
04c4448f97 | ||
|
|
0e45909c7e | ||
|
|
44366ffd3c | ||
|
|
49cf1a2cea | ||
|
|
86800d2d71 | ||
|
|
41fdc9071a | ||
|
|
14d75364eb | ||
|
|
2127128ef5 | ||
|
|
95ebba6fdb | ||
|
|
ff63d3eb23 | ||
|
|
05b8ef1727 | ||
|
|
458f2d8bf1 | ||
|
|
87a84b608c | ||
|
|
dd7a9c3b7a | ||
|
|
d1ca0d7839 | ||
|
|
3419480b98 | ||
|
|
a003a0c585 | ||
|
|
e3dfe4bc32 | ||
|
|
c9ec7aa215 | ||
|
|
7675be7da3 | ||
|
|
1a03b703f1 | ||
|
|
9165581266 | ||
|
|
6a770296f9 | ||
|
|
cbfb99a5cf | ||
|
|
bae3f64c10 | ||
|
|
7c7102d8a6 | ||
|
|
8e9ba23227 | ||
|
|
1d91e3c5bf | ||
|
|
45d4622920 | ||
|
|
b7da4b6bfe | ||
|
|
fdf51c119b | ||
|
|
6de8c91059 | ||
|
|
490f0ad329 | ||
|
|
882aea357f | ||
|
|
d8eedfa1c7 | ||
|
|
90e57c9b3c | ||
|
|
a269e7f5d9 | ||
|
|
4a3c1a4cc6 | ||
|
|
804cb36adc | ||
|
|
5aaf9ef11b | ||
|
|
c65dd1f476 | ||
|
|
05f136b3da | ||
|
|
1c4cb8c620 | ||
|
|
fec66c7dac | ||
|
|
e82f78fc84 | ||
|
|
67d6ad57c3 | ||
|
|
48f1c4667a | ||
|
|
6a9da97163 | ||
|
|
7b68cb9fe7 | ||
|
|
6691f80906 | ||
|
|
bdfe877c8f | ||
|
|
ee3668eb03 | ||
|
|
4a443c4a45 | ||
|
|
d964c23c43 | ||
|
|
20d4c64ffe | ||
|
|
bf3709f4c5 | ||
|
|
8f22d8dda4 | ||
|
|
0f6a8d3991 | ||
|
|
5b8da8c89a | ||
|
|
f2e5ea5783 | ||
|
|
cb888c343e | ||
|
|
18f37440dc | ||
|
|
3582f8733d | ||
|
|
b84dba2e8d | ||
|
|
61fc4ca8a6 | ||
|
|
bbd234b951 | ||
|
|
0545cfa121 | ||
|
|
26969b725e | ||
|
|
eb31732af0 | ||
|
|
9c08f78017 | ||
|
|
54dcbeb6cf | ||
|
|
a52aab4618 | ||
|
|
5f5a49995a | ||
|
|
ac7239732d | ||
|
|
68d537fb28 | ||
|
|
7ce071c575 | ||
|
|
42f1aba00c | ||
|
|
dfb1b4db95 | ||
|
|
6900bbc07a | ||
|
|
d4b507ddb1 | ||
|
|
1bad71f13a | ||
|
|
63b3a6f323 | ||
|
|
ba7976f3b1 | ||
|
|
b5b350fdcf | ||
|
|
87e292414d | ||
|
|
5e3ebbde5e | ||
|
|
6d9d2a3304 | ||
|
|
eabb25c796 |
@@ -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
|
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 🛑
|
🛑 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/
|
/.idea/
|
||||||
.env
|
.env
|
||||||
build/
|
build/
|
||||||
.vscode/
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
fixtures/typings-test/*.js
|
fixtures/typings-test/*.js
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ lib/test/
|
|||||||
docs/
|
docs/
|
||||||
/.idea/
|
/.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
bin/
|
||||||
build/
|
build/
|
||||||
fixtures/
|
fixtures/
|
||||||
coverage/
|
coverage/
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
language: node_js
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
node_js:
|
|
||||||
- 6
|
|
||||||
before_install:
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
|
|
||||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
|
|
||||||
- npm install -g npm@5.1.0
|
|
||||||
env:
|
|
||||||
matrix:
|
|
||||||
- NPM_COMMAND=tsc
|
|
||||||
- NPM_COMMAND=lint
|
|
||||||
- NPM_COMMAND=test-coverage
|
|
||||||
notifications:
|
|
||||||
email: false
|
|
||||||
script: npm run $NPM_COMMAND
|
|
||||||
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,130 +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>
|
|
||||||
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>
|
|
||||||
jpoth <poth.john@gmail.com>
|
|
||||||
Justin Luk <jluk@users.noreply.github.com>
|
|
||||||
Justin Mecham <justin@mecham.me>
|
|
||||||
Kirill Merkushev <lanwen@yandex.ru>
|
|
||||||
Krasimir Tsonev <krasimir@outset.ws>
|
|
||||||
Ledion Bitincka <lbitincka@gmail.com>
|
|
||||||
Linus Unnebäck <linus@folkdatorn.se>
|
|
||||||
Luca <LucaT1@users.noreply.github.com>
|
|
||||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
|
||||||
Lukas Drgon <lukas.drgon@gmail.com>
|
|
||||||
Lukas Geiger <lukas.geiger94@gmail.com>
|
|
||||||
Maël Nison <nison.mael@gmail.com>
|
|
||||||
Marc Dumais <marc.dumais@ericsson.com>
|
|
||||||
Marek Libra <mlibra@redhat.com>
|
|
||||||
Markus F.X.J. Oberhumer <markus@oberhumer.com>
|
|
||||||
Martin Chloride <i@martincl2.me>
|
|
||||||
Martin Koppehel <martin.koppehel@st.ovgu.de>
|
|
||||||
Martin Wang <jiahaow@ca.ibm.com>
|
|
||||||
Matt Bierner <matb@microsoft.com>
|
|
||||||
Matthew James <matthew.d.james87@gmail.com>
|
|
||||||
Michael Irwin <mikesir87@gmail.com>
|
|
||||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
|
||||||
mofux <t.zilz@mofux.org>
|
|
||||||
muji <muji@tmpfs.org>
|
|
||||||
Nicolas Ramz <nicolas.ramz@gmail.com>
|
|
||||||
npezza93 <npezza93@gmail.com>
|
|
||||||
Oleksandr Andriienko <oandriie@redhat.com>
|
|
||||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
|
||||||
Paris Kasidiaris <paris@sourcelair.com>
|
|
||||||
Peng Xiao <pengxiao@outlook.com>
|
|
||||||
Peter Baumgarten <me@peterbaumgarten.com>
|
|
||||||
Philip Olson <philip.olson@protonmail.ch>
|
|
||||||
PowerHat <31401273+7PH@users.noreply.github.com>
|
|
||||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
|
||||||
pro-src <rodneyd.teal@gmail.com>
|
|
||||||
Rick Baker <rick@ricktbaker.com>
|
|
||||||
runarberg <runar@greenqloud.com>
|
|
||||||
Saad Malik <simfox3@gmail.com>
|
|
||||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
|
||||||
Saswat Das <saswatds@users.noreply.github.com>
|
|
||||||
Saul Costa <saul@codevolve.com>
|
|
||||||
Shuanglei Tao <tsl0922@gmail.com>
|
|
||||||
sitzmar <sitzmar@gmail.com>
|
|
||||||
Steven Silvester <steven.silvester@ieee.org>
|
|
||||||
stuicey <stuicey@users.noreply.github.com>
|
|
||||||
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>
|
|
||||||
+9
-11
@@ -23,34 +23,32 @@ opening an issue, read these pointers.
|
|||||||
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
||||||
exact steps a maintainer has to take to make the problem occur.
|
exact steps a maintainer has to take to make the problem occur.
|
||||||
|
|
||||||
- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem.
|
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
|
||||||
|
|
||||||
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||||
bottom of the pile.
|
bottom of the pile.
|
||||||
|
|
||||||
## Contributing code
|
## Contributing code
|
||||||
|
|
||||||
- Make sure you have a [GitHub account](https://github.com/join)
|
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/)
|
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
([how to fork a repo](https://help.github.com/articles/fork-a-repo)).
|
||||||
- Make your changes
|
- 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.
|
- 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).
|
- Follow the general code style of the rest of the project (see below).
|
||||||
- Submit a pull request
|
- Submit a pull request
|
||||||
([how to create a pull request](https://help.github.com/articles/fork-a-repo)).
|
([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.
|
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).
|
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
|
in question. (Either you hold all rights on the code, or the rights
|
||||||
holder has explicitly granted the right to use it like this,
|
holder has explicitly granted the right to use it like this,
|
||||||
through a compatible open source license or through a direct
|
through a compatible open source license or through a direct
|
||||||
agreement with you.)
|
agreement with you.)
|
||||||
|
|
||||||
### Third party dependencies
|
|
||||||
|
|
||||||
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
|
|
||||||
|
|||||||
+1
-1
@@ -16,4 +16,4 @@ RUN npm install
|
|||||||
COPY . /usr/src/app
|
COPY . /usr/src/app
|
||||||
|
|
||||||
# Run the tests and build, to make sure everything is working nicely
|
# Run the tests and build, to make sure everything is working nicely
|
||||||
RUN npm run build && npm run webpack && npm run test
|
RUN npm run build && npm run test
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
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) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,23 @@
|
|||||||
# [](https://xtermjs.org)
|
# [](https://xtermjs.org)
|
||||||
|
|
||||||
[](https://travis-ci.org/xtermjs/xterm.js) [](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) [](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)
|
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||||
|
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||||
|
|
||||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
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.
|
||||||
|
|
||||||
It enables applications to provide fully featured terminals to their users and create great development experiences.
|
|
||||||
|
|
||||||
## Features
|
## 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.
|
- **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.
|
||||||
- **Mouse events support**: Xterm.js captures mouse events like click and scroll and passes them to the terminal's back-end controlling process
|
- **Perfomant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||||
- **CJK (Chinese, Japanese, Korean) character support**: Xterm.js renders CJK characters seamlessly
|
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||||
- **IME support**: Insert international (including CJK) characters using IME input with your keyboard
|
- **Self-contained**: Requires zero dependencies to work.
|
||||||
- **Self-contained library**: Xterm.js works on its own. It does not require any external libraries like jQuery or React to work
|
- **Accessible**: Screen reader support can be turned on using the `screenReaderMode` option.
|
||||||
- **Modular, event-based API**: Lets you build addons and themes with ease
|
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||||
|
|
||||||
## What xterm.js is not
|
## 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
|
## Getting Started
|
||||||
|
|
||||||
@@ -27,7 +27,7 @@ First you need to install the module, we ship exclusively through [npm](https://
|
|||||||
npm install xterm
|
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
|
```html
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
@@ -41,17 +41,15 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
|||||||
<script>
|
<script>
|
||||||
var term = new Terminal();
|
var term = new Terminal();
|
||||||
term.open(document.getElementById('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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
```
|
```
|
||||||
|
|
||||||
Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
|
||||||
|
|
||||||
### Importing
|
### 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
|
```javascript
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
@@ -59,42 +57,34 @@ import { Terminal } from 'xterm';
|
|||||||
|
|
||||||
### Addons
|
### 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:
|
To use an addon, just import the JavaScript module and pass it to `Terminal`'s `applyAddon` method:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { Terminal } from xterm;
|
import { Terminal } from 'xterm';
|
||||||
import * as fit from 'xterm/lib/addons/fit/fit';
|
import * as fit from 'xterm/lib/addons/fit/fit';
|
||||||
|
|
||||||
|
|
||||||
Terminal.applyAddon(fit);
|
Terminal.applyAddon(fit);
|
||||||
|
|
||||||
var xterm = new Terminal(); // Instantiate the terminal
|
var xterm = new Terminal(); // Instantiate the terminal
|
||||||
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You will also need to include the addon's CSS file if it has one in the folder.
|
||||||
|
|
||||||
#### Importing Addons in TypeScript
|
#### 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()`.
|
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.
|
||||||
|
|
||||||
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
|
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||||
const xterm = new Terminal();
|
const xterm = new Terminal();
|
||||||
|
|
||||||
// Fit the terminal when necessary:
|
fit(xterm); // Fit the terminal when necessary
|
||||||
fit(xterm);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Third party addons
|
|
||||||
|
|
||||||
There are also the following third party addons available:
|
|
||||||
|
|
||||||
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
|
|
||||||
|
|
||||||
## Browser Support
|
## 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:
|
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:
|
||||||
@@ -105,22 +95,28 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
|||||||
- Safari latest
|
- Safari latest
|
||||||
- IE11
|
- 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 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
|
## Real-world uses
|
||||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
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
|
- [**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
|
- [**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
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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`.
|
- [**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.
|
- [**WebSSH2**](https://github.com/billchurch/WebSSH2): A web based SSH2 client using xterm.js, socket.io, and ssh2.
|
||||||
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
- [**Spyder Terminal**](https://github.com/spyder-ide/spyder-terminal): A full fledged system terminal embedded on Spyder IDE.
|
||||||
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
- [**Cloud Commander**](https://cloudcmd.io "Cloud Commander"): Orthodox web file manager with console and editor.
|
||||||
- [**Codevolve**](https://www.codevolve.com "Codevolve"): Online platform for interactive coding and web development courses. Live container-backed terminal uses `xterm.js`.
|
- [**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.
|
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
||||||
@@ -128,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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
|
||||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||||
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.
|
- [**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.
|
- [**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.
|
- [**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.
|
||||||
@@ -141,122 +136,57 @@ computational environment for Jupyter, supporting interactive data science and s
|
|||||||
- [**Kubebox**](https://github.com/astefanutti/kubebox): Terminal console for Kubernetes clusters.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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
|
- [**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.
|
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
|
||||||
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
- [**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.
|
||||||
|
- [**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)
|
||||||
|
|
||||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
||||||
|
|
||||||
## Demo
|
|
||||||
|
|
||||||
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
|
|
||||||
|
|
||||||
Below you can find instructions on how to run the demo on different platforms.
|
|
||||||
|
|
||||||
### SourceLair
|
|
||||||
|
|
||||||
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
|
|
||||||
|
|
||||||
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
|
|
||||||
|
|
||||||
### Docker
|
|
||||||
|
|
||||||
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer).
|
|
||||||
|
|
||||||
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
|
|
||||||
|
|
||||||
To run the just demo (with no editing access). run the following command in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker run -p 3000:3000 xtermjs/xterm.js
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
To run the demo and builder in parallel, run the following command in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
|
|
||||||
|
|
||||||
### Foreman (or other Procfile runner)
|
|
||||||
|
|
||||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
### Linux or macOS
|
|
||||||
|
|
||||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start # Run this in its own terminal
|
|
||||||
npm run watch # Run this in its own terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
|
|
||||||
|
|
||||||
```
|
|
||||||
npm install
|
|
||||||
npm start # Run this in its own terminal
|
|
||||||
npm run watch # Run this in its own terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
Then open http://127.0.0.1:3000 in a web browser to access the demo.
|
|
||||||
|
|
||||||
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
Tests are run using the following npm scripts:
|
|
||||||
|
|
||||||
- `npm test`: This will run both unit tests and the linter
|
|
||||||
- `npm run test-suite <file>`: This will run all tests within a particular file, <file> is the test file name excluding the extension (eg. "Linkifier.test")
|
|
||||||
- `npm run test-debug`: This will run unit tests with `--inspect-brk`, this can then be automatically debugged using [VS Code auto attach](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_auto-attach-feature) or manually attached to by a debugger
|
|
||||||
- `npm run test-coverage`: This will run all unit tests and produce a coverage report
|
|
||||||
- `npm run lint`: This will run the linter only
|
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
Xterm.js follows a monthly release cycle roughly.
|
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).
|
||||||
|
|
||||||
## Development and Contribution
|
### Beta builds
|
||||||
|
|
||||||
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
|
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
|
||||||
|
|
||||||
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
|
```
|
||||||
|
npm install -S xterm@beta
|
||||||
|
```
|
||||||
|
|
||||||
### Code structure
|
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.
|
||||||
|
|
||||||
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code ran when launching a web worker.
|
## Contributing
|
||||||
|
|
||||||
|
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||||
|
|
||||||
## License Agreement
|
## License Agreement
|
||||||
|
|
||||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
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) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||||
|
|||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Node.js
|
||||||
|
# Build a general Node.js application with npm.
|
||||||
|
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||||
|
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
- job: Linux
|
||||||
|
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 mocha
|
||||||
|
displayName: 'Unit tests'
|
||||||
|
- script: |
|
||||||
|
yarn lint
|
||||||
|
displayName: 'Lint'
|
||||||
|
|
||||||
|
- job: macOS
|
||||||
|
pool:
|
||||||
|
vmImage: 'xcode9-macos10.13'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '8.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
- script: |
|
||||||
|
yarn
|
||||||
|
displayName: 'Install dependencies and build'
|
||||||
|
- script: |
|
||||||
|
yarn mocha
|
||||||
|
displayName: 'Unit tests'
|
||||||
|
- script: |
|
||||||
|
yarn lint
|
||||||
|
displayName: 'Lint'
|
||||||
|
- script: |
|
||||||
|
yarn test-coverage
|
||||||
|
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCHNAME
|
||||||
|
yarn coveralls
|
||||||
|
displayName: 'Generate and publish coverage'
|
||||||
|
|
||||||
|
- job: Windows
|
||||||
|
pool:
|
||||||
|
vmImage: 'vs2017-win2016'
|
||||||
|
steps:
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: '8.x'
|
||||||
|
displayName: 'Install Node.js'
|
||||||
|
- script: |
|
||||||
|
yarn
|
||||||
|
displayName: 'Install dependencies and build'
|
||||||
|
- script: |
|
||||||
|
yarn mocha
|
||||||
|
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
|
|
||||||
-88
@@ -1,88 +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.get('/', function(req, res){
|
|
||||||
res.sendFile(__dirname + '/index.html');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/style.css', function(req, res){
|
|
||||||
res.sendFile(__dirname + '/style.css');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/dist/bundle.js', function(req, res){
|
|
||||||
res.sendFile(__dirname + '/dist/bundle.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/dist/bundle.js.map', function(req, res){
|
|
||||||
res.sendFile(__dirname + '/dist/bundle.js.map');
|
|
||||||
});
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
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);
|
|
||||||
+304
@@ -0,0 +1,304 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* This file is the entry point for browserify.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference path="../typings/xterm.d.ts"/>
|
||||||
|
|
||||||
|
import { Terminal } from '../lib/public/Terminal';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// 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, ITerminalOptions } from 'xterm';
|
||||||
|
|
||||||
|
export interface IWindowWithTerminal extends Window {
|
||||||
|
term: TerminalType;
|
||||||
|
Terminal?: typeof TerminalType;
|
||||||
|
}
|
||||||
|
declare let window: IWindowWithTerminal;
|
||||||
|
|
||||||
|
Terminal.applyAddon(fit);
|
||||||
|
|
||||||
|
let term;
|
||||||
|
let searchAddon: SearchAddon;
|
||||||
|
let protocol;
|
||||||
|
let socketURL;
|
||||||
|
let socket;
|
||||||
|
let pid;
|
||||||
|
|
||||||
|
const terminalContainer = document.getElementById('terminal-container');
|
||||||
|
const actionElements = {
|
||||||
|
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||||
|
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
|
||||||
|
};
|
||||||
|
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||||
|
|
||||||
|
function setPadding(): void {
|
||||||
|
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||||
|
term.fit();
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
if (term) {
|
||||||
|
term.dispose();
|
||||||
|
term = null;
|
||||||
|
window.term = null;
|
||||||
|
socket = null;
|
||||||
|
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
createTerminal();
|
||||||
|
document.getElementById('dispose').innerHTML = 'Dispose terminal';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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.onResize((size: { cols: number, rows: number }) => {
|
||||||
|
if (!pid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cols = size.cols;
|
||||||
|
const rows = size.rows;
|
||||||
|
const 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();
|
||||||
|
term.focus();
|
||||||
|
|
||||||
|
addDomListener(paddingElement, 'change', setPadding);
|
||||||
|
|
||||||
|
addDomListener(actionElements.findNext, 'keyup', (e) => {
|
||||||
|
const searchOptions = getSearchOptions();
|
||||||
|
searchOptions.incremental = e.key !== `Enter`;
|
||||||
|
searchAddon.findNext(actionElements.findNext.value, searchOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
addDomListener(actionElements.findPrevious, 'keyup', (e) => {
|
||||||
|
if (e.key === `Enter`) {
|
||||||
|
searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// fit is called within a setTimeout, cols and rows need this.
|
||||||
|
setTimeout(() => {
|
||||||
|
initOptions(term);
|
||||||
|
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
|
||||||
|
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
|
||||||
|
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
|
||||||
|
paddingElement.value = '0';
|
||||||
|
|
||||||
|
// Set terminal size again to set the specific dimensions on the demo
|
||||||
|
updateTerminalSize();
|
||||||
|
|
||||||
|
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
|
||||||
|
res.text().then((processId) => {
|
||||||
|
pid = processId;
|
||||||
|
socketURL += processId;
|
||||||
|
socket = new WebSocket(socketURL);
|
||||||
|
socket.onopen = runRealTerminal;
|
||||||
|
socket.onclose = runFakeTerminal;
|
||||||
|
socket.onerror = runFakeTerminal;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function runRealTerminal(): void {
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
function runFakeTerminal(): void {
|
||||||
|
if (term._initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
term._initialized = true;
|
||||||
|
|
||||||
|
term.prompt = () => {
|
||||||
|
term.write('\r\n$ ');
|
||||||
|
};
|
||||||
|
|
||||||
|
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.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._core.buffer.x > 2) {
|
||||||
|
term.write('\b \b');
|
||||||
|
}
|
||||||
|
} else if (printable) {
|
||||||
|
term.write(e.key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initOptions(term: TerminalType): void {
|
||||||
|
const blacklistedOptions = [
|
||||||
|
// Internal only options
|
||||||
|
'cancelEvents',
|
||||||
|
'convertEol',
|
||||||
|
'debug',
|
||||||
|
'handler',
|
||||||
|
'screenKeys',
|
||||||
|
'termName',
|
||||||
|
'useFlowControl',
|
||||||
|
// Complex option
|
||||||
|
'theme'
|
||||||
|
];
|
||||||
|
const stringOptions = {
|
||||||
|
bellSound: null,
|
||||||
|
bellStyle: ['none', 'sound'],
|
||||||
|
cursorStyle: ['block', 'underline', 'bar'],
|
||||||
|
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
||||||
|
fontFamily: null,
|
||||||
|
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||||
|
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||||
|
rendererType: ['dom', 'canvas']
|
||||||
|
};
|
||||||
|
const options = Object.keys((<any>term)._core.options);
|
||||||
|
const booleanOptions = [];
|
||||||
|
const numberOptions = [];
|
||||||
|
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||||
|
switch (typeof term.getOption(o)) {
|
||||||
|
case 'boolean':
|
||||||
|
booleanOptions.push(o);
|
||||||
|
break;
|
||||||
|
case 'number':
|
||||||
|
numberOptions.push(o);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (Object.keys(stringOptions).indexOf(o) === -1) {
|
||||||
|
console.warn(`Unrecognized option: "${o}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
html += '<div class="option-group">';
|
||||||
|
booleanOptions.forEach(o => {
|
||||||
|
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
||||||
|
});
|
||||||
|
html += '</div><div class="option-group">';
|
||||||
|
numberOptions.forEach(o => {
|
||||||
|
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' ? '0.1' : '1'}"/></label></div>`;
|
||||||
|
});
|
||||||
|
html += '</div><div class="option-group">';
|
||||||
|
Object.keys(stringOptions).forEach(o => {
|
||||||
|
if (stringOptions[o]) {
|
||||||
|
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||||
|
} else {
|
||||||
|
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
const container = document.getElementById('options-container');
|
||||||
|
container.innerHTML = html;
|
||||||
|
|
||||||
|
// Attach listeners
|
||||||
|
booleanOptions.forEach(o => {
|
||||||
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
|
addDomListener(input, 'change', () => {
|
||||||
|
console.log('change', o, input.checked);
|
||||||
|
term.setOption(o, input.checked);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
numberOptions.forEach(o => {
|
||||||
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
|
addDomListener(input, 'change', () => {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
Object.keys(stringOptions).forEach(o => {
|
||||||
|
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||||
|
addDomListener(input, 'change', () => {
|
||||||
|
console.log('change', o, input.value);
|
||||||
|
term.setOption(o, input.value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||||
|
element.addEventListener(type, handler);
|
||||||
|
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||||
|
}
|
||||||
|
|
||||||
|
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._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();
|
||||||
|
}
|
||||||
+6
-3
@@ -2,8 +2,8 @@
|
|||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>xterm.js demo</title>
|
<title>xterm.js demo</title>
|
||||||
<link rel="stylesheet" href="/build/xterm.css" />
|
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
<link rel="stylesheet" href="/src/xterm.css" />
|
||||||
<link rel="stylesheet" href="style.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/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="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
<p>
|
<p>
|
||||||
<label>Find next <input id="find-next"/></label>
|
<label>Find next <input id="find-next"/></label>
|
||||||
<label>Find previous <input id="find-previous"/></label>
|
<label>Find previous <input id="find-previous"/></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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -33,6 +36,6 @@
|
|||||||
<hr/>
|
<hr/>
|
||||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||||
<script src="dist/bundle.js" defer ></script>
|
<script src="dist/client-bundle.js" defer ></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
-271
@@ -1,271 +0,0 @@
|
|||||||
import * as Terminal from '../build/xterm';
|
|
||||||
import * as attach from '../build/addons/attach/attach';
|
|
||||||
import * as fit from '../build/addons/fit/fit';
|
|
||||||
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
|
|
||||||
import * as search from '../build/addons/search/search';
|
|
||||||
import * as webLinks from '../build/addons/webLinks/webLinks';
|
|
||||||
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
|
|
||||||
|
|
||||||
|
|
||||||
Terminal.applyAddon(attach);
|
|
||||||
Terminal.applyAddon(fit);
|
|
||||||
Terminal.applyAddon(fullscreen);
|
|
||||||
Terminal.applyAddon(search);
|
|
||||||
Terminal.applyAddon(webLinks);
|
|
||||||
Terminal.applyAddon(winptyCompat);
|
|
||||||
|
|
||||||
|
|
||||||
var term,
|
|
||||||
protocol,
|
|
||||||
socketURL,
|
|
||||||
socket,
|
|
||||||
pid;
|
|
||||||
|
|
||||||
var terminalContainer = document.getElementById('terminal-container'),
|
|
||||||
actionElements = {
|
|
||||||
findNext: document.querySelector('#find-next'),
|
|
||||||
findPrevious: document.querySelector('#find-previous')
|
|
||||||
},
|
|
||||||
paddingElement = document.getElementById('padding');
|
|
||||||
|
|
||||||
function setPadding() {
|
|
||||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
|
||||||
term.fit();
|
|
||||||
}
|
|
||||||
|
|
||||||
createTerminal();
|
|
||||||
|
|
||||||
const disposeRecreateButtonHandler = () => {
|
|
||||||
// If the terminal exists dispose of it, otherwise recreate it
|
|
||||||
if (term) {
|
|
||||||
term.dispose();
|
|
||||||
term = null;
|
|
||||||
window.term = null;
|
|
||||||
socket = null;
|
|
||||||
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
createTerminal();
|
|
||||||
document.getElementById('dispose').innerHTML = 'Dispose terminal';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
|
||||||
|
|
||||||
function createTerminal() {
|
|
||||||
// Clean terminal
|
|
||||||
while (terminalContainer.children.length) {
|
|
||||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
|
||||||
}
|
|
||||||
term = new Terminal({});
|
|
||||||
window.term = term; // Expose `term` to window for debugging purposes
|
|
||||||
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.winptyCompatInit();
|
|
||||||
term.webLinksInit();
|
|
||||||
term.fit();
|
|
||||||
term.focus();
|
|
||||||
|
|
||||||
addDomListener(paddingElement, 'change', setPadding);
|
|
||||||
|
|
||||||
addDomListener(actionElements.findNext, 'keypress', function (e) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
term.findNext(actionElements.findNext.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
e.preventDefault();
|
|
||||||
term.findPrevious(actionElements.findPrevious.value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// fit is called within a setTimeout, cols and rows need this.
|
|
||||||
setTimeout(function () {
|
|
||||||
initOptions(term);
|
|
||||||
document.getElementById(`opt-cols`).value = term.cols;
|
|
||||||
document.getElementById(`opt-rows`).value = term.rows;
|
|
||||||
paddingElement.value = 0;
|
|
||||||
|
|
||||||
// Set terminal size again to set the specific dimensions on the demo
|
|
||||||
updateTerminalSize();
|
|
||||||
|
|
||||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
|
||||||
|
|
||||||
res.text().then(function (processId) {
|
|
||||||
pid = processId;
|
|
||||||
socketURL += processId;
|
|
||||||
socket = new WebSocket(socketURL);
|
|
||||||
socket.onopen = runRealTerminal;
|
|
||||||
socket.onclose = runFakeTerminal;
|
|
||||||
socket.onerror = runFakeTerminal;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
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._core.register(term.addDisposableListener('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._core.register(term.addDisposableListener('paste', function (data, ev) {
|
|
||||||
term.write(data);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function initOptions(term) {
|
|
||||||
var blacklistedOptions = [
|
|
||||||
// Internal only options
|
|
||||||
'cancelEvents',
|
|
||||||
'convertEol',
|
|
||||||
'debug',
|
|
||||||
'handler',
|
|
||||||
'screenKeys',
|
|
||||||
'termName',
|
|
||||||
'useFlowControl',
|
|
||||||
// Complex option
|
|
||||||
'theme',
|
|
||||||
// Only in constructor
|
|
||||||
'rendererType'
|
|
||||||
];
|
|
||||||
var stringOptions = {
|
|
||||||
bellSound: null,
|
|
||||||
bellStyle: ['none', 'sound'],
|
|
||||||
cursorStyle: ['block', 'underline', 'bar'],
|
|
||||||
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
|
||||||
fontFamily: null,
|
|
||||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
|
||||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900']
|
|
||||||
};
|
|
||||||
var options = Object.keys(term._core.options);
|
|
||||||
var booleanOptions = [];
|
|
||||||
var numberOptions = [];
|
|
||||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
|
||||||
switch (typeof term.getOption(o)) {
|
|
||||||
case 'boolean':
|
|
||||||
booleanOptions.push(o);
|
|
||||||
break;
|
|
||||||
case 'number':
|
|
||||||
numberOptions.push(o);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (Object.keys(stringOptions).indexOf(o) === -1) {
|
|
||||||
console.warn(`Unrecognized option: "${o}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var html = '';
|
|
||||||
html += '<div class="option-group">';
|
|
||||||
booleanOptions.forEach(o => {
|
|
||||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
|
||||||
});
|
|
||||||
html += '</div><div class="option-group">';
|
|
||||||
numberOptions.forEach(o => {
|
|
||||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
|
|
||||||
});
|
|
||||||
html += '</div><div class="option-group">';
|
|
||||||
Object.keys(stringOptions).forEach(o => {
|
|
||||||
if (stringOptions[o]) {
|
|
||||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
|
||||||
} else {
|
|
||||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
html += '</div>';
|
|
||||||
|
|
||||||
var container = document.getElementById('options-container');
|
|
||||||
container.innerHTML = html;
|
|
||||||
|
|
||||||
// Attach listeners
|
|
||||||
booleanOptions.forEach(o => {
|
|
||||||
var input = document.getElementById(`opt-${o}`);
|
|
||||||
addDomListener(input, 'change', () => {
|
|
||||||
console.log('change', o, input.checked);
|
|
||||||
term.setOption(o, input.checked);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
numberOptions.forEach(o => {
|
|
||||||
var input = document.getElementById(`opt-${o}`);
|
|
||||||
addDomListener(input, 'change', () => {
|
|
||||||
console.log('change', o, input.value);
|
|
||||||
if (o === 'cols' || o === 'rows') {
|
|
||||||
updateTerminalSize();
|
|
||||||
} else {
|
|
||||||
term.setOption(o, parseInt(input.value, 10));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Object.keys(stringOptions).forEach(o => {
|
|
||||||
var input = document.getElementById(`opt-${o}`);
|
|
||||||
addDomListener(input, 'change', () => {
|
|
||||||
console.log('change', o, input.value);
|
|
||||||
term.setOption(o, input.value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function addDomListener(element, type, handler) {
|
|
||||||
element.addEventListener(type, handler);
|
|
||||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateTerminalSize() {
|
|
||||||
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
|
|
||||||
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
|
|
||||||
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
|
||||||
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
|
||||||
terminalContainer.style.width = width;
|
|
||||||
terminalContainer.style.height = height;
|
|
||||||
term.fit();
|
|
||||||
}
|
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
var express = require('express');
|
||||||
|
var expressWs = require('express-ws');
|
||||||
|
var os = require('os');
|
||||||
|
var pty = require('node-pty');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether to use UTF8 binary transport.
|
||||||
|
* (Must also be switched in client.ts)
|
||||||
|
*/
|
||||||
|
const USE_BINARY_UTF8 = false;
|
||||||
|
|
||||||
|
|
||||||
|
function startServer() {
|
||||||
|
var app = express();
|
||||||
|
expressWs(app);
|
||||||
|
|
||||||
|
var terminals = {},
|
||||||
|
logs = {};
|
||||||
|
|
||||||
|
app.use('/src', express.static(__dirname + '/../src'));
|
||||||
|
app.get('/logo.png', (req, res) => res.sendFile(__dirname + '/logo.png'));
|
||||||
|
|
||||||
|
app.get('/', function(req, res){
|
||||||
|
res.sendFile(__dirname + '/index.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/test', function(req, res){
|
||||||
|
res.sendFile(__dirname + '/test.html');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/style.css', function(req, res){
|
||||||
|
res.sendFile(__dirname + '/style.css');
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/dist/client-bundle.js', function(req, res){
|
||||||
|
res.sendFile(__dirname + '/dist/client-bundle.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', [], {
|
||||||
|
name: 'xterm-color',
|
||||||
|
cols: cols || 80,
|
||||||
|
rows: rows || 24,
|
||||||
|
cwd: process.env.PWD,
|
||||||
|
env: process.env,
|
||||||
|
encoding: USE_BINARY_UTF8 ? null : 'utf8'
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
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];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* This file is the entry point for browserify.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
const startServer = require('./server.js');
|
||||||
|
|
||||||
|
startServer();
|
||||||
|
|
||||||
|
// Build/watch client source
|
||||||
|
const clientConfig = {
|
||||||
|
entry: path.resolve(__dirname, 'client.ts'),
|
||||||
|
devtool: 'inline-source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
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: {
|
||||||
|
filename: 'client-bundle.js',
|
||||||
|
path: path.resolve(__dirname, 'dist')
|
||||||
|
},
|
||||||
|
mode: 'development',
|
||||||
|
watch: true
|
||||||
|
};
|
||||||
|
const compiler = webpack(clientConfig);
|
||||||
|
|
||||||
|
compiler.watch({
|
||||||
|
// Example watchOptions
|
||||||
|
aggregateTimeout: 300,
|
||||||
|
poll: undefined
|
||||||
|
}, (err, stats) => {
|
||||||
|
// Print watch/build result here...
|
||||||
|
console.log(stats.toString({
|
||||||
|
colors: true
|
||||||
|
}));
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"rootDir": ".",
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"client.ts",
|
||||||
|
"../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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:
|
volumes:
|
||||||
- ./:/usr/src/app
|
- ./:/usr/src/app
|
||||||
ports:
|
ports:
|
||||||
- ${XTERMJS_PORT:3000}:3000
|
- ${XTERMJS_PORT:-3000}:3000
|
||||||
command: ["npm", "start"]
|
command: ["npm", "start"]
|
||||||
|
|
||||||
watch:
|
watch:
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5"
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6"
|
||||||
|
],
|
||||||
|
"noEmit": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
/// <reference path="../../typings/xterm.d.ts" />
|
/// <reference path="../../typings/xterm.d.ts" />
|
||||||
|
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal, IDisposable } from 'xterm';
|
||||||
|
|
||||||
namespace constructor {
|
namespace constructor {
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ namespace constructor {
|
|||||||
'disableStdin': false,
|
'disableStdin': false,
|
||||||
'rows': 1,
|
'rows': 1,
|
||||||
'scrollback': 10,
|
'scrollback': 10,
|
||||||
'tabStopWidth': 2,
|
'tabStopWidth': 2
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +119,12 @@ namespace methods_core {
|
|||||||
const t: Terminal = new Terminal();
|
const t: Terminal = new Terminal();
|
||||||
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
|
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
|
||||||
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
|
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 {
|
namespace options {
|
||||||
{
|
{
|
||||||
@@ -149,6 +155,7 @@ namespace methods_core {
|
|||||||
const r25: string = t.getOption('fontWeightBold');
|
const r25: string = t.getOption('fontWeightBold');
|
||||||
const r26: boolean = t.getOption('allowTransparency');
|
const r26: boolean = t.getOption('allowTransparency');
|
||||||
const r27: boolean = t.getOption('rightClickSelectsWord');
|
const r27: boolean = t.getOption('rightClickSelectsWord');
|
||||||
|
const r28: boolean = t.getOption('windowsMode');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const t: Terminal = new Terminal();
|
const t: Terminal = new Terminal();
|
||||||
@@ -171,6 +178,7 @@ namespace methods_core {
|
|||||||
t.setOption('useFlowControl', true);
|
t.setOption('useFlowControl', true);
|
||||||
t.setOption('allowTransparency', true);
|
t.setOption('allowTransparency', true);
|
||||||
t.setOption('visualBell', true);
|
t.setOption('visualBell', true);
|
||||||
|
t.setOption('windowsMode', true);
|
||||||
t.setOption('colors', ['a', 'b']);
|
t.setOption('colors', ['a', 'b']);
|
||||||
t.setOption('letterSpacing', 1);
|
t.setOption('letterSpacing', 1);
|
||||||
t.setOption('cols', 1);
|
t.setOption('cols', 1);
|
||||||
|
|||||||
+5
-17
@@ -14,12 +14,11 @@ const source = require('vinyl-source-stream');
|
|||||||
const sourcemaps = require('gulp-sourcemaps');
|
const sourcemaps = require('gulp-sourcemaps');
|
||||||
const ts = require('gulp-typescript');
|
const ts = require('gulp-typescript');
|
||||||
const util = require('gulp-util');
|
const util = require('gulp-util');
|
||||||
const webpack = require('webpack-stream');
|
|
||||||
|
|
||||||
const buildDir = process.env.BUILD_DIR || 'build';
|
const buildDir = process.env.BUILD_DIR || 'build';
|
||||||
const tsProject = ts.createProject('tsconfig.json');
|
const tsProject = ts.createProject('src/tsconfig.json');
|
||||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
let srcDir = './src';
|
||||||
let outDir = tsProject.config.compilerOptions.outDir;
|
let outDir = './lib';
|
||||||
|
|
||||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||||
|
|
||||||
@@ -62,7 +61,7 @@ gulp.task('browserify', function() {
|
|||||||
};
|
};
|
||||||
let bundleStream = browserify(browserifyOptions)
|
let bundleStream = browserify(browserifyOptions)
|
||||||
.bundle()
|
.bundle()
|
||||||
.pipe(source('xterm.js'))
|
.pipe(source(`xterm.js`))
|
||||||
.pipe(buffer())
|
.pipe(buffer())
|
||||||
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
.pipe(sourcemaps.init({loadMaps: true, sourceRoot: '..'}))
|
||||||
.pipe(sourcemaps.write('./'))
|
.pipe(sourcemaps.write('./'))
|
||||||
@@ -137,17 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
gulp.task('webpack', ['build'], function() {
|
gulp.task('build', ['css', 'sorcery', 'sorcery-addons']);
|
||||||
return gulp.src('demo/main.js')
|
|
||||||
.pipe(webpack(require('./webpack.config.js')))
|
|
||||||
.pipe(gulp.dest('demo/dist/'));
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
gulp.task('watch-demo', ['webpack'], () => {
|
|
||||||
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
|
|
||||||
});
|
|
||||||
|
|
||||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
|
||||||
gulp.task('test', ['mocha']);
|
gulp.task('test', ['mocha']);
|
||||||
gulp.task('default', ['build']);
|
gulp.task('default', ['build']);
|
||||||
|
|||||||
+24
-16
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm",
|
"name": "xterm",
|
||||||
"description": "Full xterm terminal, in your browser",
|
"description": "Full xterm terminal, in your browser",
|
||||||
"version": "3.5.1",
|
"version": "3.14.0",
|
||||||
"main": "lib/public/Terminal.js",
|
"main": "lib/public/Terminal.js",
|
||||||
"types": "typings/xterm.d.ts",
|
"types": "typings/xterm.d.ts",
|
||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
@@ -12,9 +12,11 @@
|
|||||||
"@types/jsdom": "11.0.1",
|
"@types/jsdom": "11.0.1",
|
||||||
"@types/mocha": "^2.2.33",
|
"@types/mocha": "^2.2.33",
|
||||||
"@types/node": "6.0.108",
|
"@types/node": "6.0.108",
|
||||||
|
"@types/puppeteer": "^1.12.4",
|
||||||
|
"@types/utf8": "^2.1.6",
|
||||||
|
"@types/webpack": "^4.4.11",
|
||||||
"browserify": "^13.3.0",
|
"browserify": "^13.3.0",
|
||||||
"chai": "3.5.0",
|
"chai": "3.5.0",
|
||||||
"concurrently": "^3.5.1",
|
|
||||||
"coveralls": "^3.0.1",
|
"coveralls": "^3.0.1",
|
||||||
"express": "4.13.4",
|
"express": "4.13.4",
|
||||||
"express-ws": "2.0.0-rc.1",
|
"express-ws": "2.0.0-rc.1",
|
||||||
@@ -27,39 +29,45 @@
|
|||||||
"gulp-sourcemaps": "1.9.1",
|
"gulp-sourcemaps": "1.9.1",
|
||||||
"gulp-typescript": "^3.1.3",
|
"gulp-typescript": "^3.1.3",
|
||||||
"gulp-util": "3.0.8",
|
"gulp-util": "3.0.8",
|
||||||
"jsdoc": "3.4.3",
|
|
||||||
"jsdom": "^11.11.0",
|
"jsdom": "^11.11.0",
|
||||||
"merge-stream": "^1.0.1",
|
"merge-stream": "^1.0.1",
|
||||||
"node-pty": "0.7.6",
|
"node-pty": "0.7.6",
|
||||||
"nodemon": "1.10.2",
|
"nodemon": "1.10.2",
|
||||||
"npm-run-all": "^4.1.2",
|
|
||||||
"nyc": "^11.8.0",
|
"nyc": "^11.8.0",
|
||||||
|
"puppeteer": "^1.15.0",
|
||||||
"sorcery": "^0.10.0",
|
"sorcery": "^0.10.0",
|
||||||
|
"source-map-loader": "^0.2.4",
|
||||||
|
"ts-loader": "^4.5.0",
|
||||||
"tslint": "^5.9.1",
|
"tslint": "^5.9.1",
|
||||||
"tslint-consistent-codestyle": "^1.13.0",
|
"tslint-consistent-codestyle": "^1.13.0",
|
||||||
"typescript": "2.8.3",
|
"typescript": "3.5",
|
||||||
|
"utf8": "^3.0.0",
|
||||||
"vinyl-buffer": "^1.0.0",
|
"vinyl-buffer": "^1.0.0",
|
||||||
"vinyl-source-stream": "^1.1.0",
|
"vinyl-source-stream": "^1.1.0",
|
||||||
"webpack": "^3.10.0",
|
"webpack": "^4.17.1",
|
||||||
"webpack-stream": "^4.0.0",
|
"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"
|
"zmodem.js": "^0.1.5"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
|
"start": "node demo/start",
|
||||||
|
"start-debug": "node --inspect-brk demo/start",
|
||||||
"start-zmodem": "node demo/zmodem/app",
|
"start-zmodem": "node demo/zmodem/app",
|
||||||
"lint": "tslint 'src/**/*.ts'",
|
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||||
"test": "npm-run-all mocha lint",
|
"test": "npm run mocha",
|
||||||
|
"posttest": "npm run lint",
|
||||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||||
"test-suite": "gulp mocha-suite --test",
|
"test-suite": "gulp mocha-suite --test",
|
||||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||||
|
"test-api": "mocha \"**/*.api.js\"",
|
||||||
"mocha": "gulp test",
|
"mocha": "gulp test",
|
||||||
"build:docs": "jsdoc -c jsdoc.json",
|
"prebuild": "tsc -b ./src/tsconfig.all.json",
|
||||||
"tsc": "tsc",
|
|
||||||
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
|
|
||||||
"build": "gulp build",
|
"build": "gulp build",
|
||||||
"prepublish": "npm run build",
|
"prepare": "npm run prebuild",
|
||||||
|
"prepublishOnly": "npm run build",
|
||||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||||
"webpack": "gulp webpack",
|
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
|
||||||
"watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-31
@@ -5,10 +5,12 @@
|
|||||||
|
|
||||||
import * as Strings from './Strings';
|
import * as Strings from './Strings';
|
||||||
import { ITerminal, IBuffer } from './Types';
|
import { ITerminal, IBuffer } from './Types';
|
||||||
import { isMac } from './shared/utils/Browser';
|
import { isMac } from './common/Platform';
|
||||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||||
import { Disposable } from './common/Lifecycle';
|
import { Disposable } from './common/Lifecycle';
|
||||||
|
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
|
||||||
|
import { IRenderDimensions } from './renderer/Types';
|
||||||
|
|
||||||
const MAX_ROWS_TO_READ = 20;
|
const MAX_ROWS_TO_READ = 20;
|
||||||
|
|
||||||
@@ -25,6 +27,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
private _liveRegionLineCount: number = 0;
|
private _liveRegionLineCount: number = 0;
|
||||||
|
|
||||||
private _renderRowsDebouncer: RenderDebouncer;
|
private _renderRowsDebouncer: RenderDebouncer;
|
||||||
|
private _screenDprMonitor: ScreenDprMonitor;
|
||||||
|
|
||||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||||
@@ -40,7 +43,12 @@ export class AccessibilityManager extends Disposable {
|
|||||||
*/
|
*/
|
||||||
private _charsToConsume: string[] = [];
|
private _charsToConsume: string[] = [];
|
||||||
|
|
||||||
constructor(private _terminal: ITerminal) {
|
private _charsToAnnounce: string = '';
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private _terminal: ITerminal,
|
||||||
|
private _dimensions: IRenderDimensions
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
this._accessibilityTreeRoot = document.createElement('div');
|
this._accessibilityTreeRoot = document.createElement('div');
|
||||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||||
@@ -61,7 +69,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
this._refreshRowsDimensions();
|
this._refreshRowsDimensions();
|
||||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
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._refreshRows();
|
||||||
|
|
||||||
this._liveRegion = document.createElement('div');
|
this._liveRegion = document.createElement('div');
|
||||||
@@ -72,22 +80,21 @@ export class AccessibilityManager extends Disposable {
|
|||||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||||
|
|
||||||
this.register(this._renderRowsDebouncer);
|
this.register(this._renderRowsDebouncer);
|
||||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
|
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
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
|
// 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('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('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()));
|
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
|
this._screenDprMonitor = new ScreenDprMonitor();
|
||||||
// guarenteed
|
this.register(this._screenDprMonitor);
|
||||||
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
this._screenDprMonitor.setListener(() => this._refreshRowsDimensions());
|
||||||
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
|
||||||
// This shouldn't be needed on modern browsers but is present in case the
|
// 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()));
|
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -157,7 +164,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onResize(cols: number, rows: number): void {
|
private _onResize(rows: number): void {
|
||||||
// Remove bottom boundary listener
|
// Remove bottom boundary listener
|
||||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||||
|
|
||||||
@@ -197,10 +204,10 @@ export class AccessibilityManager extends Disposable {
|
|||||||
// Have the screen reader ignore the char if it was just input
|
// Have the screen reader ignore the char if it was just input
|
||||||
const shiftedChar = this._charsToConsume.shift();
|
const shiftedChar = this._charsToConsume.shift();
|
||||||
if (shiftedChar !== char) {
|
if (shiftedChar !== char) {
|
||||||
this._announceCharacter(char);
|
this._charsToAnnounce += char;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this._announceCharacter(char);
|
this._charsToAnnounce += char;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (char === '\n') {
|
if (char === '\n') {
|
||||||
@@ -239,7 +246,7 @@ export class AccessibilityManager extends Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _refreshRows(start?: number, end?: number): void {
|
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 {
|
private _renderRows(start: number, end: number): void {
|
||||||
@@ -249,32 +256,41 @@ export class AccessibilityManager extends Disposable {
|
|||||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||||
const element = this._rowElements[i];
|
const element = this._rowElements[i];
|
||||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
if (element) {
|
||||||
element.setAttribute('aria-posinset', posInSet);
|
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||||
element.setAttribute('aria-setsize', setSize);
|
element.setAttribute('aria-posinset', posInSet);
|
||||||
|
element.setAttribute('aria-setsize', setSize);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
this._announceCharacters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshRowsDimensions(): void {
|
private _refreshRowsDimensions(): void {
|
||||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
if (!this._dimensions.actualCellHeight) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (this._rowElements.length !== this._terminal.rows) {
|
||||||
|
this._onResize(this._terminal.rows);
|
||||||
|
}
|
||||||
for (let i = 0; i < this._terminal.rows; i++) {
|
for (let i = 0; i < this._terminal.rows; i++) {
|
||||||
this._refreshRowDimensions(this._rowElements[i]);
|
this._refreshRowDimensions(this._rowElements[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _refreshRowDimensions(element: HTMLElement): void {
|
public setDimensions(dimensions: IRenderDimensions): void {
|
||||||
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
|
this._dimensions = dimensions;
|
||||||
|
this._refreshRowsDimensions();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _announceCharacter(char: string): void {
|
private _refreshRowDimensions(element: HTMLElement): void {
|
||||||
if (char === ' ') {
|
element.style.height = `${this._dimensions.actualCellHeight}px`;
|
||||||
// Always use nbsp for spaces in order to preserve the space between characters in
|
}
|
||||||
// voiceover's caption window
|
|
||||||
this._liveRegion.innerHTML += ' ';
|
private _announceCharacters(): void {
|
||||||
} else {
|
if (this._charsToAnnounce.length === 0) {
|
||||||
this._liveRegion.textContent += char;
|
return;
|
||||||
}
|
}
|
||||||
|
this._liveRegion.textContent += this._charsToAnnounce;
|
||||||
|
this._charsToAnnounce = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1109
-55
File diff suppressed because it is too large
Load Diff
+408
-112
@@ -3,16 +3,13 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CircularList } from './common/CircularList';
|
import { CircularList, IInsertEvent } from './common/CircularList';
|
||||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||||
import { IMarker } from 'xterm';
|
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) | (257 << 9) | (256 << 0);
|
|
||||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
|
||||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
|
||||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
|
||||||
export const CHAR_DATA_CODE_INDEX = 3;
|
|
||||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,7 +20,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
|||||||
* - scroll position
|
* - scroll position
|
||||||
*/
|
*/
|
||||||
export class Buffer implements IBuffer {
|
export class Buffer implements IBuffer {
|
||||||
public lines: CircularList<LineData>;
|
public lines: CircularList<IBufferLine>;
|
||||||
public ydisp: number;
|
public ydisp: number;
|
||||||
public ybase: number;
|
public ybase: number;
|
||||||
public y: number;
|
public y: number;
|
||||||
@@ -33,7 +30,12 @@ export class Buffer implements IBuffer {
|
|||||||
public tabs: any;
|
public tabs: any;
|
||||||
public savedY: number;
|
public savedY: number;
|
||||||
public savedX: number;
|
public savedX: number;
|
||||||
|
public savedCurAttrData = DEFAULT_ATTR_DATA.clone();
|
||||||
public markers: Marker[] = [];
|
public markers: Marker[] = [];
|
||||||
|
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.
|
* Create a new Buffer.
|
||||||
@@ -45,17 +47,45 @@ export class Buffer implements IBuffer {
|
|||||||
private _terminal: ITerminal,
|
private _terminal: ITerminal,
|
||||||
private _hasScrollback: boolean
|
private _hasScrollback: boolean
|
||||||
) {
|
) {
|
||||||
|
this._cols = this._terminal.cols;
|
||||||
|
this._rows = this._terminal.rows;
|
||||||
this.clear();
|
this.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getNullCell(attr?: IAttributeData): ICellData {
|
||||||
|
if (attr) {
|
||||||
|
this._nullCell.fg = attr.fg;
|
||||||
|
this._nullCell.bg = attr.bg;
|
||||||
|
} else {
|
||||||
|
this._nullCell.fg = 0;
|
||||||
|
this._nullCell.bg = 0;
|
||||||
|
}
|
||||||
|
return this._nullCell;
|
||||||
|
}
|
||||||
|
|
||||||
|
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: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||||
|
return new BufferLine(this._terminal.cols, this.getNullCell(attr), isWrapped);
|
||||||
|
}
|
||||||
|
|
||||||
public get hasScrollback(): boolean {
|
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 {
|
public get isCursorInViewport(): boolean {
|
||||||
const absoluteY = this.ybase + this.y;
|
const absoluteY = this.ybase + this.y;
|
||||||
const relativeY = absoluteY - this.ydisp;
|
const relativeY = absoluteY - this.ydisp;
|
||||||
return (relativeY >= 0 && relativeY < this._terminal.rows);
|
return (relativeY >= 0 && relativeY < this._rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,11 +106,14 @@ export class Buffer implements IBuffer {
|
|||||||
/**
|
/**
|
||||||
* Fills the buffer's viewport with blank lines.
|
* Fills the buffer's viewport with blank lines.
|
||||||
*/
|
*/
|
||||||
public fillViewportRows(): void {
|
public fillViewportRows(fillAttr?: IAttributeData): void {
|
||||||
if (this.lines.length === 0) {
|
if (this.lines.length === 0) {
|
||||||
let i = this._terminal.rows;
|
if (fillAttr === undefined) {
|
||||||
|
fillAttr = DEFAULT_ATTR_DATA;
|
||||||
|
}
|
||||||
|
let i = this._rows;
|
||||||
while (i--) {
|
while (i--) {
|
||||||
this.lines.push(this._terminal.blankLine());
|
this.lines.push(this.getBlankLine(fillAttr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +126,9 @@ export class Buffer implements IBuffer {
|
|||||||
this.ybase = 0;
|
this.ybase = 0;
|
||||||
this.y = 0;
|
this.y = 0;
|
||||||
this.x = 0;
|
this.x = 0;
|
||||||
this.lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
|
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._rows));
|
||||||
this.scrollTop = 0;
|
this.scrollTop = 0;
|
||||||
this.scrollBottom = this._terminal.rows - 1;
|
this.scrollBottom = this._rows - 1;
|
||||||
this.setupTabStops();
|
this.setupTabStops();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +138,9 @@ export class Buffer implements IBuffer {
|
|||||||
* @param newRows The new number of rows.
|
* @param newRows The new number of rows.
|
||||||
*/
|
*/
|
||||||
public resize(newCols: number, newRows: number): void {
|
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
|
// Increase max length if needed before adjustments to allow space to fill
|
||||||
// as required.
|
// as required.
|
||||||
const newMaxLength = this._getCorrectBufferLength(newRows);
|
const newMaxLength = this._getCorrectBufferLength(newRows);
|
||||||
@@ -115,20 +151,17 @@ export class Buffer implements IBuffer {
|
|||||||
// The following adjustments should only happen if the buffer has been
|
// The following adjustments should only happen if the buffer has been
|
||||||
// initialized/filled.
|
// initialized/filled.
|
||||||
if (this.lines.length > 0) {
|
if (this.lines.length > 0) {
|
||||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
// Deal with columns increasing (reducing needs to happen after reflow)
|
||||||
if (this._terminal.cols < newCols) {
|
if (this._cols < newCols) {
|
||||||
const ch: CharData = [DEFAULT_ATTR, ' ', 1, 32]; // does xterm use the default attr?
|
|
||||||
for (let i = 0; i < this.lines.length; i++) {
|
for (let i = 0; i < this.lines.length; i++) {
|
||||||
while (this.lines.get(i).length < newCols) {
|
this.lines.get(i).resize(newCols, nullCell);
|
||||||
this.lines.get(i).push(ch);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resize rows in both directions as needed
|
// Resize rows in both directions as needed
|
||||||
let addToY = 0;
|
let addToY = 0;
|
||||||
if (this._terminal.rows < newRows) {
|
if (this._rows < newRows) {
|
||||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
for (let y = this._rows; y < newRows; y++) {
|
||||||
if (this.lines.length < newRows + this.ybase) {
|
if (this.lines.length < newRows + this.ybase) {
|
||||||
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
|
if (this.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,
|
// There is room above the buffer and there are no empty elements below the line,
|
||||||
@@ -142,12 +175,12 @@ export class Buffer implements IBuffer {
|
|||||||
} else {
|
} else {
|
||||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
// 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
|
// are blank lines after the cursor
|
||||||
this.lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
this.lines.push(new BufferLine(newCols, nullCell));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // (this._terminal.rows >= newRows)
|
} else { // (this._rows >= newRows)
|
||||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
for (let y = this._rows; y > newRows; y--) {
|
||||||
if (this.lines.length > newRows + this.ybase) {
|
if (this.lines.length > newRows + this.ybase) {
|
||||||
if (this.lines.length > this.ybase + this.y + 1) {
|
if (this.lines.length > this.ybase + this.y + 1) {
|
||||||
// The line is a blank line below the cursor, remove it
|
// The line is a blank line below the cursor, remove it
|
||||||
@@ -187,6 +220,271 @@ export class Buffer implements IBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.scrollBottom = newRows - 1;
|
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
|
||||||
|
* (default in translateBufferLineToString).
|
||||||
|
* The method also works on wrapped line strings given rows were not trimmed.
|
||||||
|
* The method operates on the CharData string length, there are no
|
||||||
|
* additional content or boundary checks. Therefore the string and the buffer
|
||||||
|
* should not be altered in between.
|
||||||
|
* TODO: respect trim flag after fixing #1685
|
||||||
|
* @param lineIndex line index the string was retrieved from
|
||||||
|
* @param stringIndex index within the string
|
||||||
|
* @param startCol column offset the string was retrieved from
|
||||||
|
*/
|
||||||
|
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number, trimRight: boolean = false): BufferIndex {
|
||||||
|
while (stringIndex) {
|
||||||
|
const line = this.lines.get(lineIndex);
|
||||||
|
if (!line) {
|
||||||
|
return [-1, -1];
|
||||||
|
}
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lineIndex++;
|
||||||
|
}
|
||||||
|
return [lineIndex, 0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -199,76 +497,23 @@ export class Buffer implements IBuffer {
|
|||||||
* @param startCol The column to start at.
|
* @param startCol The column to start at.
|
||||||
* @param endCol The column to end at.
|
* @param endCol The column to end at.
|
||||||
*/
|
*/
|
||||||
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol: number = null): string {
|
public translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol: number = 0, endCol?: number): string {
|
||||||
// Get full line
|
|
||||||
let lineString = '';
|
|
||||||
const line = this.lines.get(lineIndex);
|
const line = this.lines.get(lineIndex);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
return line.translateToString(trimRight, startCol, endCol);
|
||||||
// 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[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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
||||||
let first = y;
|
let first = y;
|
||||||
let last = y;
|
let last = y;
|
||||||
// Scan upwards for wrapped lines
|
// Scan upwards for wrapped lines
|
||||||
while (first > 0 && (<any>this.lines.get(first)).isWrapped) {
|
while (first > 0 && this.lines.get(first).isWrapped) {
|
||||||
first--;
|
first--;
|
||||||
}
|
}
|
||||||
// Scan downwards for wrapped lines
|
// Scan downwards for wrapped lines
|
||||||
while (last + 1 < this.lines.length && (<any>this.lines.get(last + 1)).isWrapped) {
|
while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) {
|
||||||
last++;
|
last++;
|
||||||
}
|
}
|
||||||
return { first, last };
|
return { first, last };
|
||||||
@@ -279,7 +524,7 @@ export class Buffer implements IBuffer {
|
|||||||
* @param i The index to start setting up tab stops from.
|
* @param i The index to start setting up tab stops from.
|
||||||
*/
|
*/
|
||||||
public setupTabStops(i?: number): void {
|
public setupTabStops(i?: number): void {
|
||||||
if (i != null) {
|
if (i !== null && i !== undefined) {
|
||||||
if (!this.tabs[i]) {
|
if (!this.tabs[i]) {
|
||||||
i = this.prevStop(i);
|
i = this.prevStop(i);
|
||||||
}
|
}
|
||||||
@@ -288,7 +533,7 @@ export class Buffer implements IBuffer {
|
|||||||
i = 0;
|
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;
|
this.tabs[i] = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,11 +543,11 @@ export class Buffer implements IBuffer {
|
|||||||
* @param x The position to move the cursor to the previous tab stop.
|
* @param x The position to move the cursor to the previous tab stop.
|
||||||
*/
|
*/
|
||||||
public prevStop(x?: number): number {
|
public prevStop(x?: number): number {
|
||||||
if (x == null) {
|
if (x === null || x === undefined) {
|
||||||
x = this.x;
|
x = this.x;
|
||||||
}
|
}
|
||||||
while (!this.tabs[--x] && x > 0);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -310,54 +555,105 @@ export class Buffer implements IBuffer {
|
|||||||
* @param x The position to move the cursor one tab stop forward.
|
* @param x The position to move the cursor one tab stop forward.
|
||||||
*/
|
*/
|
||||||
public nextStop(x?: number): number {
|
public nextStop(x?: number): number {
|
||||||
if (x == null) {
|
if (x === null || x === undefined) {
|
||||||
x = this.x;
|
x = this.x;
|
||||||
}
|
}
|
||||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
while (!this.tabs[++x] && x < this._cols);
|
||||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
return x >= this._cols ? this._cols - 1 : x < 0 ? 0 : x;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addMarker(y: number): Marker {
|
public addMarker(y: number): Marker {
|
||||||
const marker = new Marker(y);
|
const marker = new Marker(y);
|
||||||
this.markers.push(marker);
|
this.markers.push(marker);
|
||||||
marker.register(this.lines.addDisposableListener('trim', amount => {
|
marker.register(this.lines.onTrim(amount => {
|
||||||
marker.line -= amount;
|
marker.line -= amount;
|
||||||
// The marker should be disposed when the line is trimmed from the buffer
|
// The marker should be disposed when the line is trimmed from the buffer
|
||||||
if (marker.line < 0) {
|
if (marker.line < 0) {
|
||||||
marker.dispose();
|
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;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _removeMarker(marker: Marker): void {
|
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);
|
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
|
||||||
|
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Marker extends EventEmitter implements IMarker {
|
/**
|
||||||
private static _nextId = 1;
|
* Iterator to get unwrapped content strings from the buffer.
|
||||||
|
* The iterator returns at least the string data between the borders
|
||||||
|
* `startIndex` and `endIndex` (exclusive) and will expand the lines
|
||||||
|
* by `startOverscan` to the top and by `endOverscan` to the bottom,
|
||||||
|
* if no new line was found in between.
|
||||||
|
* It will never read/return string data beyond `startIndex - startOverscan`
|
||||||
|
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
|
||||||
|
* It is possible to always get the full string for the first and last line as well
|
||||||
|
* by setting the overscan values to the actual buffer length. This not recommended
|
||||||
|
* since it might return the whole buffer within a single string in a worst case scenario.
|
||||||
|
*/
|
||||||
|
export class BufferStringIterator implements IBufferStringIterator {
|
||||||
|
private _current: number;
|
||||||
|
|
||||||
private _id: number = Marker._nextId++;
|
constructor (
|
||||||
public isDisposed: boolean = false;
|
private _buffer: IBuffer,
|
||||||
|
private _trimRight: boolean,
|
||||||
public get id(): number { return this._id; }
|
private _startIndex: number = 0,
|
||||||
|
private _endIndex: number = _buffer.lines.length,
|
||||||
constructor(
|
private _startOverscan: number = 0,
|
||||||
public line: number
|
private _endOverscan: number = 0
|
||||||
) {
|
) {
|
||||||
super();
|
if (this._startIndex < 0) {
|
||||||
|
this._startIndex = 0;
|
||||||
|
}
|
||||||
|
if (this._endIndex > this._buffer.lines.length) {
|
||||||
|
this._endIndex = this._buffer.lines.length;
|
||||||
|
}
|
||||||
|
this._current = this._startIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public hasNext(): boolean {
|
||||||
if (this.isDisposed) {
|
return this._current < this._endIndex;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
public next(): IBufferStringIteratorResult {
|
||||||
|
const range = this._buffer.getWrappedRangeForLine(this._current);
|
||||||
|
// limit search window to overscan value at both borders
|
||||||
|
if (range.first < this._startIndex - this._startOverscan) {
|
||||||
|
range.first = this._startIndex - this._startOverscan;
|
||||||
}
|
}
|
||||||
this.isDisposed = true;
|
if (range.last > this._endIndex + this._endOverscan) {
|
||||||
// Emit before super.dispose such that dispose listeners get a change to react
|
range.last = this._endIndex + this._endOverscan;
|
||||||
this.emit('dispose');
|
}
|
||||||
super.dispose();
|
// limit to current buffer length
|
||||||
|
range.first = Math.max(range.first, 0);
|
||||||
|
range.last = Math.min(range.last, this._buffer.lines.length);
|
||||||
|
let result = '';
|
||||||
|
for (let i = range.first; i <= range.last; ++i) {
|
||||||
|
result += this._buffer.translateBufferLineToString(i, this._trimRight);
|
||||||
|
}
|
||||||
|
this._current = range.last + 1;
|
||||||
|
return {range: range, content: result};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-3
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
|||||||
import { ITerminal } from './Types';
|
import { ITerminal } from './Types';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { Buffer } from './Buffer';
|
import { Buffer } from './Buffer';
|
||||||
import { MockTerminal } from './utils/TestUtils.test';
|
import { MockTerminal } from './TestUtils.test';
|
||||||
|
|
||||||
describe('BufferSet', () => {
|
describe('BufferSet', () => {
|
||||||
let terminal: ITerminal;
|
let terminal: ITerminal;
|
||||||
@@ -15,8 +15,8 @@ describe('BufferSet', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
terminal = new MockTerminal();
|
terminal = new MockTerminal();
|
||||||
terminal.cols = 80;
|
(terminal as any).cols = 80;
|
||||||
terminal.rows = 24;
|
(terminal as any).rows = 24;
|
||||||
terminal.options.scrollback = 1000;
|
terminal.options.scrollback = 1000;
|
||||||
bufferSet = new BufferSet(terminal);
|
bufferSet = new BufferSet(terminal);
|
||||||
});
|
});
|
||||||
@@ -48,4 +48,30 @@ describe('BufferSet', () => {
|
|||||||
assert.equal(bufferSet.active, bufferSet.alt);
|
assert.equal(bufferSet.active, bufferSet.alt);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('cursor handling when swapping buffers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
bufferSet.normal.x = 0;
|
||||||
|
bufferSet.normal.y = 0;
|
||||||
|
bufferSet.alt.x = 0;
|
||||||
|
bufferSet.alt.y = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the cursor stationary when activating alt buffer', () => {
|
||||||
|
bufferSet.activateNormalBuffer();
|
||||||
|
bufferSet.active.x = 30;
|
||||||
|
bufferSet.active.y = 10;
|
||||||
|
bufferSet.activateAltBuffer();
|
||||||
|
assert.equal(bufferSet.active.x, 30);
|
||||||
|
assert.equal(bufferSet.active.y, 10);
|
||||||
|
});
|
||||||
|
it('should keep the cursor stationary when activating normal buffer', () => {
|
||||||
|
bufferSet.activateAltBuffer();
|
||||||
|
bufferSet.active.x = 30;
|
||||||
|
bufferSet.active.y = 10;
|
||||||
|
bufferSet.activateNormalBuffer();
|
||||||
|
assert.equal(bufferSet.active.x, 30);
|
||||||
|
assert.equal(bufferSet.active.y, 10);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+17
-12
@@ -3,25 +3,29 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, IBufferSet } from './Types';
|
import { ITerminal, IBufferSet, IBuffer } from './Types';
|
||||||
|
import { IAttributeData } from './core/Types';
|
||||||
import { Buffer } from './Buffer';
|
import { Buffer } from './Buffer';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||||
* provides also utilities for working with them.
|
* provides also utilities for working with them.
|
||||||
*/
|
*/
|
||||||
export class BufferSet extends EventEmitter implements IBufferSet {
|
export class BufferSet implements IBufferSet {
|
||||||
private _normal: Buffer;
|
private _normal: Buffer;
|
||||||
private _alt: Buffer;
|
private _alt: Buffer;
|
||||||
private _activeBuffer: 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.
|
* Create a new BufferSet for the given terminal.
|
||||||
* @param {Terminal} terminal - The terminal the BufferSet will belong to
|
* @param _terminal - The terminal the BufferSet will belong to
|
||||||
*/
|
*/
|
||||||
constructor(private _terminal: ITerminal) {
|
constructor(private _terminal: ITerminal) {
|
||||||
super();
|
|
||||||
this._normal = new Buffer(this._terminal, true);
|
this._normal = new Buffer(this._terminal, true);
|
||||||
this._normal.fillViewportRows();
|
this._normal.fillViewportRows();
|
||||||
|
|
||||||
@@ -35,7 +39,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the alt Buffer of the BufferSet
|
* Returns the alt Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get alt(): Buffer {
|
public get alt(): Buffer {
|
||||||
return this._alt;
|
return this._alt;
|
||||||
@@ -43,7 +46,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the normal Buffer of the BufferSet
|
* Returns the normal Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get active(): Buffer {
|
public get active(): Buffer {
|
||||||
return this._activeBuffer;
|
return this._activeBuffer;
|
||||||
@@ -51,7 +53,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the currently active Buffer of the BufferSet
|
* Returns the currently active Buffer of the BufferSet
|
||||||
* @returns {Buffer}
|
|
||||||
*/
|
*/
|
||||||
public get normal(): Buffer {
|
public get normal(): Buffer {
|
||||||
return this._normal;
|
return this._normal;
|
||||||
@@ -64,12 +65,14 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
if (this._activeBuffer === this._normal) {
|
if (this._activeBuffer === this._normal) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._normal.x = this._alt.x;
|
||||||
|
this._normal.y = this._alt.y;
|
||||||
// The alt buffer should always be cleared when we switch to the normal
|
// The alt buffer should always be cleared when we switch to the normal
|
||||||
// buffer. This frees up memory since the alt buffer should always be new
|
// buffer. This frees up memory since the alt buffer should always be new
|
||||||
// when activated.
|
// when activated.
|
||||||
this._alt.clear();
|
this._alt.clear();
|
||||||
this._activeBuffer = this._normal;
|
this._activeBuffer = this._normal;
|
||||||
this.emit('activate', {
|
this._onBufferActivate.fire({
|
||||||
activeBuffer: this._normal,
|
activeBuffer: this._normal,
|
||||||
inactiveBuffer: this._alt
|
inactiveBuffer: this._alt
|
||||||
});
|
});
|
||||||
@@ -78,15 +81,17 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
|||||||
/**
|
/**
|
||||||
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
* Sets the alt Buffer of the BufferSet as its currently active Buffer
|
||||||
*/
|
*/
|
||||||
public activateAltBuffer(): void {
|
public activateAltBuffer(fillAttr?: IAttributeData): void {
|
||||||
if (this._activeBuffer === this._alt) {
|
if (this._activeBuffer === this._alt) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Since the alt buffer is always cleared when the normal buffer is
|
// Since the alt buffer is always cleared when the normal buffer is
|
||||||
// activated, we want to fill it when switching to it.
|
// activated, we want to fill it when switching to it.
|
||||||
this._alt.fillViewportRows();
|
this._alt.fillViewportRows(fillAttr);
|
||||||
|
this._alt.x = this._normal.x;
|
||||||
|
this._alt.y = this._normal.y;
|
||||||
this._activeBuffer = this._alt;
|
this._activeBuffer = this._alt;
|
||||||
this.emit('activate', {
|
this._onBufferActivate.fire({
|
||||||
activeBuffer: this._alt,
|
activeBuffer: this._alt,
|
||||||
inactiveBuffer: this._normal
|
inactiveBuffer: this._normal
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import jsdom = require('jsdom');
|
import jsdom = require('jsdom');
|
||||||
import { ICharMeasure } from '../Types';
|
import { ICharMeasure } from './Types';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { CharMeasure } from './CharMeasure';
|
import { CharMeasure } from './CharMeasure';
|
||||||
|
|
||||||
@@ -3,23 +3,25 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICharMeasure, ITerminalOptions } from '../Types';
|
import { ICharMeasure, ITerminalOptions } from './Types';
|
||||||
import { EventEmitter } from '../EventEmitter';
|
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility class that measures the size of a character. Measurements are done in
|
* 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
|
* the DOM rather than with a canvas context because support for extracting the
|
||||||
* height of characters is patchy across browsers.
|
* height of characters is patchy across browsers.
|
||||||
*/
|
*/
|
||||||
export class CharMeasure extends EventEmitter implements ICharMeasure {
|
export class CharMeasure implements ICharMeasure {
|
||||||
private _document: Document;
|
private _document: Document;
|
||||||
private _parentElement: HTMLElement;
|
private _parentElement: HTMLElement;
|
||||||
private _measureElement: HTMLElement;
|
private _measureElement: HTMLElement;
|
||||||
private _width: number;
|
private _width: number;
|
||||||
private _height: number;
|
private _height: number;
|
||||||
|
|
||||||
|
private _onCharSizeChanged = new EventEmitter2<void>();
|
||||||
|
public get onCharSizeChanged(): IEvent<void> { return this._onCharSizeChanged.event; }
|
||||||
|
|
||||||
constructor(document: Document, parentElement: HTMLElement) {
|
constructor(document: Document, parentElement: HTMLElement) {
|
||||||
super();
|
|
||||||
this._document = document;
|
this._document = document;
|
||||||
this._parentElement = parentElement;
|
this._parentElement = parentElement;
|
||||||
this._measureElement = this._document.createElement('span');
|
this._measureElement = this._document.createElement('span');
|
||||||
@@ -38,7 +40,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public measure(options: ITerminalOptions): void {
|
public measure(options: ITerminalOptions): void {
|
||||||
this._measureElement.style.fontFamily = options.fontFamily;
|
this._measureElement.style.fontFamily = options.fontFamily;
|
||||||
this._measureElement.style.fontSize = `${options.fontSize}px`;
|
this._measureElement.style.fontSize = `${options.fontSize}px`;
|
||||||
const geometry = this._measureElement.getBoundingClientRect();
|
const geometry = this._measureElement.getBoundingClientRect();
|
||||||
// The element is likely currently display:none, we should retain the
|
// 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) {
|
if (geometry.width === 0 || geometry.height === 0) {
|
||||||
return;
|
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._width = geometry.width;
|
||||||
this._height = Math.ceil(geometry.height);
|
this._height = adjustedHeight;
|
||||||
this.emit('charsizechanged');
|
this._onCharSizeChanged.fire();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { TestTerminal } from './TestUtils.test';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { getStringCellWidth, wcwidth } from './CharWidth';
|
||||||
|
import { IBuffer } from './Types';
|
||||||
|
import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
|
|
||||||
|
describe('getStringCellWidth', function(): void {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = new TestTerminal({rows: 5, cols: 30});
|
||||||
|
});
|
||||||
|
|
||||||
|
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
|
||||||
|
let result = 0;
|
||||||
|
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.loadCell(j, new CellData()).getAsCharData();
|
||||||
|
result += ch[CHAR_DATA_WIDTH_INDEX];
|
||||||
|
// return on sentinel
|
||||||
|
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('ASCII chars', function(): void {
|
||||||
|
const input = 'This is just ASCII text.#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('combining chars', function(): void {
|
||||||
|
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('surrogate chars', function(): void {
|
||||||
|
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('surrogate combining chars', function(): void {
|
||||||
|
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('fullwidth chars', function(): void {
|
||||||
|
const input = '1234567890#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
it('fullwidth chars offset 1', function(): void {
|
||||||
|
const input = 'a1234567890#';
|
||||||
|
terminal.writeSync(input);
|
||||||
|
const s = terminal.buffer.iterator(true).next().content;
|
||||||
|
assert.equal(input, s);
|
||||||
|
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||||
|
});
|
||||||
|
// TODO: multiline tests once #1685 is resolved
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wcwidth should match all values from the old implementation', function(): void {
|
||||||
|
// old implementation
|
||||||
|
const wcwidthOld = (function(opts: {nul: number, control: number}): (ucs: number) => number {
|
||||||
|
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||||
|
// combining characters
|
||||||
|
const COMBINING_BMP = [
|
||||||
|
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||||
|
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||||
|
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||||
|
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||||
|
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||||
|
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||||
|
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||||
|
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||||
|
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||||
|
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||||
|
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||||
|
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||||
|
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||||
|
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||||
|
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||||
|
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||||
|
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||||
|
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||||
|
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||||
|
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||||
|
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||||
|
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||||
|
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||||
|
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||||
|
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||||
|
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||||
|
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||||
|
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||||
|
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||||
|
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||||
|
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||||
|
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||||
|
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||||
|
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||||
|
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||||
|
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||||
|
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||||
|
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||||
|
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||||
|
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||||
|
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||||
|
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||||
|
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
|
||||||
|
];
|
||||||
|
const COMBINING_HIGH = [
|
||||||
|
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||||
|
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||||
|
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||||
|
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||||
|
[0xE0100, 0xE01EF]
|
||||||
|
];
|
||||||
|
// binary search
|
||||||
|
function bisearch(ucs: number, data: number[][]): boolean {
|
||||||
|
let min = 0;
|
||||||
|
let max = data.length - 1;
|
||||||
|
let mid;
|
||||||
|
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (max >= min) {
|
||||||
|
mid = (min + max) >> 1;
|
||||||
|
if (ucs > data[mid][1]) {
|
||||||
|
min = mid + 1;
|
||||||
|
} else if (ucs < data[mid][0]) {
|
||||||
|
max = mid - 1;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function wcwidthBMP(ucs: number): number {
|
||||||
|
// test for 8-bit control characters
|
||||||
|
if (ucs === 0) {
|
||||||
|
return opts.nul;
|
||||||
|
}
|
||||||
|
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
||||||
|
return opts.control;
|
||||||
|
}
|
||||||
|
// binary search in table of non-spacing characters
|
||||||
|
if (bisearch(ucs, COMBINING_BMP)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
// if we arrive here, ucs is not a combining or C0/C1 control character
|
||||||
|
if (isWideBMP(ucs)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
function isWideBMP(ucs: number): boolean {
|
||||||
|
return (
|
||||||
|
ucs >= 0x1100 && (
|
||||||
|
ucs <= 0x115f || // Hangul Jamo init. consonants
|
||||||
|
ucs === 0x2329 ||
|
||||||
|
ucs === 0x232a ||
|
||||||
|
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
|
||||||
|
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
|
||||||
|
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
|
||||||
|
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
|
||||||
|
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
|
||||||
|
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
|
||||||
|
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
||||||
|
}
|
||||||
|
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
||||||
|
if (bisearch(ucs, COMBINING_HIGH)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const control = opts.control | 0;
|
||||||
|
let table: number[] | Uint32Array = null;
|
||||||
|
function initTable(): number[] | Uint32Array {
|
||||||
|
// lookup table for BMP
|
||||||
|
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
|
||||||
|
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
|
||||||
|
const ITEMSIZE = 32; // using uint32_t
|
||||||
|
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
|
||||||
|
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
|
||||||
|
table = (typeof Uint32Array === 'undefined')
|
||||||
|
? new Array(CONTAINERSIZE)
|
||||||
|
: new Uint32Array(CONTAINERSIZE);
|
||||||
|
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
||||||
|
let num = 0;
|
||||||
|
let pos = CODEPOINTS_PER_ITEM;
|
||||||
|
while (pos--) {
|
||||||
|
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
||||||
|
}
|
||||||
|
table[i] = num;
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
// get width from lookup table
|
||||||
|
// position in container : num / CODEPOINTS_PER_ITEM
|
||||||
|
// ==> n = table[Math.floor(num / 16)]
|
||||||
|
// ==> n = table[num >> 4]
|
||||||
|
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
|
||||||
|
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
|
||||||
|
// ==> m = (n % 16) * 2
|
||||||
|
// ==> m = (num & 15) << 1
|
||||||
|
// right shift to position m
|
||||||
|
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
|
||||||
|
// we are only interested in 2 LSBs, cut off higher bits
|
||||||
|
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
||||||
|
return function (num: number): number {
|
||||||
|
num = num | 0; // get asm.js like optimization under V8
|
||||||
|
if (num < 32) {
|
||||||
|
return control | 0;
|
||||||
|
}
|
||||||
|
if (num < 127) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const t = table || initTable();
|
||||||
|
if (num < 65536) {
|
||||||
|
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||||
|
}
|
||||||
|
// do a full search for high codepoints
|
||||||
|
return wcwidthHigh(num);
|
||||||
|
};
|
||||||
|
})({nul: 0, control: 0}); // configurable options
|
||||||
|
|
||||||
|
// test full BMP range old vs new implmenetation
|
||||||
|
for (let i = 0; i < 65536; ++i) {
|
||||||
|
assert.equal(wcwidth(i), wcwidthOld(i), `mismatch for i: ${i}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
+158
-158
@@ -3,169 +3,169 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { fill } from './common/TypedArrayUtils';
|
||||||
|
|
||||||
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
|
export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: number) => number {
|
||||||
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
// extracted from https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c
|
||||||
// combining characters
|
// combining characters
|
||||||
const COMBINING_BMP = [
|
const COMBINING_BMP = [
|
||||||
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
[0x0300, 0x036F], [0x0483, 0x0486], [0x0488, 0x0489],
|
||||||
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
[0x0591, 0x05BD], [0x05BF, 0x05BF], [0x05C1, 0x05C2],
|
||||||
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
[0x05C4, 0x05C5], [0x05C7, 0x05C7], [0x0600, 0x0603],
|
||||||
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
[0x0610, 0x0615], [0x064B, 0x065E], [0x0670, 0x0670],
|
||||||
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
[0x06D6, 0x06E4], [0x06E7, 0x06E8], [0x06EA, 0x06ED],
|
||||||
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
[0x070F, 0x070F], [0x0711, 0x0711], [0x0730, 0x074A],
|
||||||
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
[0x07A6, 0x07B0], [0x07EB, 0x07F3], [0x0901, 0x0902],
|
||||||
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
[0x093C, 0x093C], [0x0941, 0x0948], [0x094D, 0x094D],
|
||||||
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
[0x0951, 0x0954], [0x0962, 0x0963], [0x0981, 0x0981],
|
||||||
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
[0x09BC, 0x09BC], [0x09C1, 0x09C4], [0x09CD, 0x09CD],
|
||||||
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
[0x09E2, 0x09E3], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||||
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||||
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
[0x0A70, 0x0A71], [0x0A81, 0x0A82], [0x0ABC, 0x0ABC],
|
||||||
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
[0x0AC1, 0x0AC5], [0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD],
|
||||||
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
[0x0AE2, 0x0AE3], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||||
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
[0x0B3F, 0x0B3F], [0x0B41, 0x0B43], [0x0B4D, 0x0B4D],
|
||||||
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
[0x0B56, 0x0B56], [0x0B82, 0x0B82], [0x0BC0, 0x0BC0],
|
||||||
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
[0x0BCD, 0x0BCD], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||||
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0CBC, 0x0CBC],
|
||||||
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
[0x0CBF, 0x0CBF], [0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD],
|
||||||
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
[0x0CE2, 0x0CE3], [0x0D41, 0x0D43], [0x0D4D, 0x0D4D],
|
||||||
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
[0x0DCA, 0x0DCA], [0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6],
|
||||||
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
[0x0E31, 0x0E31], [0x0E34, 0x0E3A], [0x0E47, 0x0E4E],
|
||||||
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
[0x0EB1, 0x0EB1], [0x0EB4, 0x0EB9], [0x0EBB, 0x0EBC],
|
||||||
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
[0x0EC8, 0x0ECD], [0x0F18, 0x0F19], [0x0F35, 0x0F35],
|
||||||
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
[0x0F37, 0x0F37], [0x0F39, 0x0F39], [0x0F71, 0x0F7E],
|
||||||
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
[0x0F80, 0x0F84], [0x0F86, 0x0F87], [0x0F90, 0x0F97],
|
||||||
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
[0x0F99, 0x0FBC], [0x0FC6, 0x0FC6], [0x102D, 0x1030],
|
||||||
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
[0x1032, 0x1032], [0x1036, 0x1037], [0x1039, 0x1039],
|
||||||
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
[0x1058, 0x1059], [0x1160, 0x11FF], [0x135F, 0x135F],
|
||||||
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
[0x1712, 0x1714], [0x1732, 0x1734], [0x1752, 0x1753],
|
||||||
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
[0x1772, 0x1773], [0x17B4, 0x17B5], [0x17B7, 0x17BD],
|
||||||
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
[0x17C6, 0x17C6], [0x17C9, 0x17D3], [0x17DD, 0x17DD],
|
||||||
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
[0x180B, 0x180D], [0x18A9, 0x18A9], [0x1920, 0x1922],
|
||||||
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
[0x1927, 0x1928], [0x1932, 0x1932], [0x1939, 0x193B],
|
||||||
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
[0x1A17, 0x1A18], [0x1B00, 0x1B03], [0x1B34, 0x1B34],
|
||||||
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
[0x1B36, 0x1B3A], [0x1B3C, 0x1B3C], [0x1B42, 0x1B42],
|
||||||
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
[0x1B6B, 0x1B73], [0x1DC0, 0x1DCA], [0x1DFE, 0x1DFF],
|
||||||
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2063],
|
||||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
|
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
|
||||||
];
|
];
|
||||||
const COMBINING_HIGH = [
|
const COMBINING_HIGH = [
|
||||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||||
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
[0x10A38, 0x10A3A], [0x10A3F, 0x10A3F], [0x1D167, 0x1D169],
|
||||||
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||||
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
[0x1D242, 0x1D244], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||||
[0xE0100, 0xE01EF]
|
[0xE0100, 0xE01EF]
|
||||||
];
|
];
|
||||||
// binary search
|
// binary search
|
||||||
function bisearch(ucs: number, data: number[][]): boolean {
|
function bisearch(ucs: number, data: number[][]): boolean {
|
||||||
let min = 0;
|
let min = 0;
|
||||||
let max = data.length - 1;
|
let max = data.length - 1;
|
||||||
let mid;
|
let mid;
|
||||||
if (ucs < data[0][0] || ucs > data[max][1]) {
|
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||||
return false;
|
|
||||||
}
|
|
||||||
while (max >= min) {
|
|
||||||
mid = (min + max) >> 1;
|
|
||||||
if (ucs > data[mid][1]) {
|
|
||||||
min = mid + 1;
|
|
||||||
} else if (ucs < data[mid][0]) {
|
|
||||||
max = mid - 1;
|
|
||||||
} else {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
function wcwidthBMP(ucs: number): number {
|
while (max >= min) {
|
||||||
// test for 8-bit control characters
|
mid = (min + max) >> 1;
|
||||||
if (ucs === 0) {
|
if (ucs > data[mid][1]) {
|
||||||
return opts.nul;
|
min = mid + 1;
|
||||||
}
|
} else if (ucs < data[mid][0]) {
|
||||||
if (ucs < 32 || (ucs >= 0x7f && ucs < 0xa0)) {
|
max = mid - 1;
|
||||||
return opts.control;
|
} else {
|
||||||
}
|
return true;
|
||||||
// binary search in table of non-spacing characters
|
|
||||||
if (bisearch(ucs, COMBINING_BMP)) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
// if we arrive here, ucs is not a combining or C0/C1 control character
|
|
||||||
if (isWideBMP(ucs)) {
|
|
||||||
return 2;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
||||||
|
if (bisearch(ucs, COMBINING_HIGH)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const control = opts.control | 0;
|
||||||
|
|
||||||
|
// create lookup table for BMP plane
|
||||||
|
const table = new Uint8Array(65536);
|
||||||
|
fill(table, 1);
|
||||||
|
table[0] = opts.nul;
|
||||||
|
// control chars
|
||||||
|
fill(table, opts.control, 1, 32);
|
||||||
|
fill(table, opts.control, 0x7f, 0xa0);
|
||||||
|
|
||||||
|
// apply wide char rules first
|
||||||
|
// wide chars
|
||||||
|
fill(table, 2, 0x1100, 0x1160);
|
||||||
|
table[0x2329] = 2;
|
||||||
|
table[0x232a] = 2;
|
||||||
|
fill(table, 2, 0x2e80, 0xa4d0);
|
||||||
|
table[0x303f] = 1; // wrongly in last line
|
||||||
|
|
||||||
|
fill(table, 2, 0xac00, 0xd7a4);
|
||||||
|
fill(table, 2, 0xf900, 0xfb00);
|
||||||
|
fill(table, 2, 0xfe10, 0xfe1a);
|
||||||
|
fill(table, 2, 0xfe30, 0xfe70);
|
||||||
|
fill(table, 2, 0xff00, 0xff61);
|
||||||
|
fill(table, 2, 0xffe0, 0xffe7);
|
||||||
|
|
||||||
|
// apply combining last to ensure we overwrite
|
||||||
|
// wrongly wide set chars:
|
||||||
|
// the original algo evals combining first and falls
|
||||||
|
// through to wide check so we simply do here the opposite
|
||||||
|
// combining 0
|
||||||
|
for (let r = 0; r < COMBINING_BMP.length; ++r) {
|
||||||
|
fill(table, 0, COMBINING_BMP[r][0], COMBINING_BMP[r][1] + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function (num: number): number {
|
||||||
|
if (num < 32) {
|
||||||
|
return control | 0;
|
||||||
|
}
|
||||||
|
if (num < 127) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
function isWideBMP(ucs: number): boolean {
|
if (num < 65536) {
|
||||||
return (
|
return table[num];
|
||||||
ucs >= 0x1100 && (
|
|
||||||
ucs <= 0x115f || // Hangul Jamo init. consonants
|
|
||||||
ucs === 0x2329 ||
|
|
||||||
ucs === 0x232a ||
|
|
||||||
(ucs >= 0x2e80 && ucs <= 0xa4cf && ucs !== 0x303f) || // CJK..Yi
|
|
||||||
(ucs >= 0xac00 && ucs <= 0xd7a3) || // Hangul Syllables
|
|
||||||
(ucs >= 0xf900 && ucs <= 0xfaff) || // CJK Compat Ideographs
|
|
||||||
(ucs >= 0xfe10 && ucs <= 0xfe19) || // Vertical forms
|
|
||||||
(ucs >= 0xfe30 && ucs <= 0xfe6f) || // CJK Compat Forms
|
|
||||||
(ucs >= 0xff00 && ucs <= 0xff60) || // Fullwidth Forms
|
|
||||||
(ucs >= 0xffe0 && ucs <= 0xffe6)));
|
|
||||||
}
|
}
|
||||||
function wcwidthHigh(ucs: number): 0 | 1 | 2 {
|
// do a full search for high codepoints
|
||||||
if (bisearch(ucs, COMBINING_HIGH)) {
|
return wcwidthHigh(num);
|
||||||
return 0;
|
};
|
||||||
}
|
|
||||||
if ((ucs >= 0x20000 && ucs <= 0x2fffd) || (ucs >= 0x30000 && ucs <= 0x3fffd)) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const control = opts.control | 0;
|
|
||||||
let table: number[] | Uint32Array = null;
|
|
||||||
function initTable(): number[] | Uint32Array {
|
|
||||||
// lookup table for BMP
|
|
||||||
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
|
|
||||||
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
|
|
||||||
const ITEMSIZE = 32; // using uint32_t
|
|
||||||
const CONTAINERSIZE = CODEPOINTS * BITWIDTH / ITEMSIZE;
|
|
||||||
const CODEPOINTS_PER_ITEM = ITEMSIZE / BITWIDTH;
|
|
||||||
table = (typeof Uint32Array === 'undefined')
|
|
||||||
? new Array(CONTAINERSIZE)
|
|
||||||
: new Uint32Array(CONTAINERSIZE);
|
|
||||||
for (let i = 0; i < CONTAINERSIZE; ++i) {
|
|
||||||
let num = 0;
|
|
||||||
let pos = CODEPOINTS_PER_ITEM;
|
|
||||||
while (pos--) {
|
|
||||||
num = (num << 2) | wcwidthBMP(CODEPOINTS_PER_ITEM * i + pos);
|
|
||||||
}
|
|
||||||
table[i] = num;
|
|
||||||
}
|
|
||||||
return table;
|
|
||||||
}
|
|
||||||
// get width from lookup table
|
|
||||||
// position in container : num / CODEPOINTS_PER_ITEM
|
|
||||||
// ==> n = table[Math.floor(num / 16)]
|
|
||||||
// ==> n = table[num >> 4]
|
|
||||||
// 16 codepoints per number: FFEEDDCCBBAA99887766554433221100
|
|
||||||
// position in number : (num % CODEPOINTS_PER_ITEM) * BITWIDTH
|
|
||||||
// ==> m = (n % 16) * 2
|
|
||||||
// ==> m = (num & 15) << 1
|
|
||||||
// right shift to position m
|
|
||||||
// ==> n = n >> m e.g. m=12 000000000000FFEEDDCCBBAA99887766
|
|
||||||
// we are only interested in 2 LSBs, cut off higher bits
|
|
||||||
// ==> n = n & 3 e.g. 000000000000000000000000000000XX
|
|
||||||
return function (num: number): number {
|
|
||||||
num = num | 0; // get asm.js like optimization under V8
|
|
||||||
if (num < 32) {
|
|
||||||
return control | 0;
|
|
||||||
}
|
|
||||||
if (num < 127) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
const t = table || initTable();
|
|
||||||
if (num < 65536) {
|
|
||||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
|
||||||
}
|
|
||||||
// do a full search for high codepoints
|
|
||||||
return wcwidthHigh(num);
|
|
||||||
};
|
|
||||||
})({nul: 0, control: 0}); // configurable options
|
})({nul: 0, control: 0}); // configurable options
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the terminal cell width for a string.
|
||||||
|
*/
|
||||||
|
export function getStringCellWidth(s: string): number {
|
||||||
|
let result = 0;
|
||||||
|
const length = s.length;
|
||||||
|
for (let i = 0; i < length; ++i) {
|
||||||
|
let code = s.charCodeAt(i);
|
||||||
|
// surrogate pair first
|
||||||
|
if (0xD800 <= code && code <= 0xDBFF) {
|
||||||
|
if (++i >= length) {
|
||||||
|
// this should not happen with strings retrieved from
|
||||||
|
// Buffer.translateToString as it converts from UTF-32
|
||||||
|
// and therefore always should contain the second part
|
||||||
|
// for any other string we still have to handle it somehow:
|
||||||
|
// simply treat the lonely surrogate first as a single char (UCS-2 behavior)
|
||||||
|
return result + wcwidth(code);
|
||||||
|
}
|
||||||
|
const second = s.charCodeAt(i);
|
||||||
|
// convert surrogate pair to high codepoint only for valid second part (UTF-16)
|
||||||
|
// otherwise treat them independently (UCS-2 behavior)
|
||||||
|
if (0xDC00 <= second && second <= 0xDFFF) {
|
||||||
|
code = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
|
||||||
|
} else {
|
||||||
|
result += wcwidth(second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result += wcwidth(code);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, ISelectionManager } from '../Types';
|
import { ITerminal, ISelectionManager } from './Types';
|
||||||
|
|
||||||
interface IWindow extends Window {
|
interface IWindow extends Window {
|
||||||
clipboardData?: {
|
clipboardData?: {
|
||||||
@@ -35,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Binds copy functionality to the given terminal.
|
* Binds copy functionality to the given terminal.
|
||||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
* @param ev The original copy event to be handled
|
||||||
*/
|
*/
|
||||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
||||||
if (term.browser.isMSIE) {
|
if (term.browser.isMSIE) {
|
||||||
@@ -50,8 +50,8 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect the clipboard's data to the terminal's input handler.
|
* Redirect the clipboard's data to the terminal's input handler.
|
||||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
* @param ev The original paste event to be handled
|
||||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
* @param term The terminal on which to apply the handled paste event
|
||||||
*/
|
*/
|
||||||
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
@@ -85,26 +85,32 @@ export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
|||||||
* @param ev The original right click event to be handled.
|
* @param ev The original right click event to be handled.
|
||||||
* @param textarea The terminal's textarea.
|
* @param textarea The terminal's textarea.
|
||||||
*/
|
*/
|
||||||
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextAreaElement): void {
|
export function moveTextAreaUnderMouseCursor(ev: MouseEvent, term: ITerminal): 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';
|
|
||||||
|
|
||||||
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
|
// Reset the terminal textarea's styling
|
||||||
// Timeout needs to be long enough for click event to be handled.
|
// Timeout needs to be long enough for click event to be handled.
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
textarea.style.position = null;
|
term.textarea.style.position = null;
|
||||||
textarea.style.width = null;
|
term.textarea.style.width = null;
|
||||||
textarea.style.height = null;
|
term.textarea.style.height = null;
|
||||||
textarea.style.left = null;
|
term.textarea.style.left = null;
|
||||||
textarea.style.top = null;
|
term.textarea.style.top = null;
|
||||||
textarea.style.zIndex = null;
|
term.textarea.style.zIndex = null;
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,14 +121,14 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
|||||||
* @param selectionManager The terminal's selection manager.
|
* @param selectionManager The terminal's selection manager.
|
||||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
* @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 {
|
export function rightClickHandler(ev: MouseEvent, term: ITerminal, selectionManager: ISelectionManager, shouldSelectWord: boolean): void {
|
||||||
moveTextAreaUnderMouseCursor(ev, textarea);
|
moveTextAreaUnderMouseCursor(ev, term);
|
||||||
|
|
||||||
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
if (shouldSelectWord && !selectionManager.isClickInSelection(ev)) {
|
||||||
selectionManager.selectWordAtCursor(ev);
|
selectionManager.selectWordAtCursor(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get textarea ready to copy from the context menu
|
// Get textarea ready to copy from the context menu
|
||||||
textarea.value = selectionManager.selectionText;
|
term.textarea.value = selectionManager.selectionText;
|
||||||
textarea.select();
|
term.textarea.select();
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ export class CompositionHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the compositionupdate event, updating the composition view.
|
* Handles the compositionupdate event, updating the composition view.
|
||||||
* @param {CompositionEvent} ev The event.
|
* @param ev The event.
|
||||||
*/
|
*/
|
||||||
public compositionupdate(ev: CompositionEvent): void {
|
public compositionupdate(ev: CompositionEvent): void {
|
||||||
this._compositionView.textContent = ev.data;
|
this._compositionView.textContent = ev.data;
|
||||||
@@ -111,17 +111,17 @@ export class CompositionHelper {
|
|||||||
/**
|
/**
|
||||||
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
* Finalizes the composition, resuming regular input actions. This is called when a composition
|
||||||
* is ending.
|
* 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
|
* 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.
|
* the command is executed.
|
||||||
*/
|
*/
|
||||||
private _finalizeComposition(waitForPropogation: boolean): void {
|
private _finalizeComposition(waitForPropagation: boolean): void {
|
||||||
this._compositionView.classList.remove('active');
|
this._compositionView.classList.remove('active');
|
||||||
this._isComposing = false;
|
this._isComposing = false;
|
||||||
this._clearTextareaPosition();
|
this._clearTextareaPosition();
|
||||||
|
|
||||||
if (!waitForPropogation) {
|
if (!waitForPropagation) {
|
||||||
// Cancel any delayed composition send requests and send the input immediately.
|
// Cancel any delayed composition send requests and send the input immediately.
|
||||||
this._isSendingComposition = false;
|
this._isSendingComposition = false;
|
||||||
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
|
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
|
// 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
|
// 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
|
// complete. This ensures the correct character is retrieved.
|
||||||
// because:
|
// This solution was used because:
|
||||||
// - The compositionend event's data property is unreliable, at least on Chromium
|
// - 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 last compositionupdate event's data property does not always accurately describe
|
||||||
// the character, a counter example being Korean where an ending consonsant can move to
|
// the 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.top = cursorTop + 'px';
|
||||||
this._compositionView.style.height = cellHeight + 'px';
|
this._compositionView.style.height = cellHeight + 'px';
|
||||||
this._compositionView.style.lineHeight = 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
|
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||||
// text is.
|
// text is.
|
||||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||||
|
|||||||
+300
-137
File diff suppressed because it is too large
Load Diff
+100
-33
@@ -4,7 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
|
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
|
||||||
|
import { IDisposable } from 'xterm';
|
||||||
import { Disposable } from './common/Lifecycle';
|
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).
|
* Returns an array filled with numbers between the low and high parameters (right exclusive).
|
||||||
@@ -66,8 +75,9 @@ export class TransitionTable {
|
|||||||
const PRINTABLES = r(0x20, 0x7f);
|
const PRINTABLES = r(0x20, 0x7f);
|
||||||
const EXECUTABLES = r(0x00, 0x18);
|
const EXECUTABLES = r(0x00, 0x18);
|
||||||
EXECUTABLES.push(0x19);
|
EXECUTABLES.push(0x19);
|
||||||
EXECUTABLES.concat(r(0x1c, 0x20));
|
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.
|
* 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);
|
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
|
||||||
let state: any;
|
let state: any;
|
||||||
|
|
||||||
// table with default transition [any] --> DEFAULT_TRANSITION
|
// table with default transition
|
||||||
for (state in states) {
|
for (state in states) {
|
||||||
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small
|
// 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);
|
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(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.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(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
|
// csi entries
|
||||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
table.addMany(r(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.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||||
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, 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.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;
|
return table;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -192,7 +204,7 @@ export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
|||||||
*/
|
*/
|
||||||
class DcsDummy implements IDcsHandler {
|
class DcsDummy implements IDcsHandler {
|
||||||
hook(collect: string, params: number[], flag: number): void { }
|
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 { }
|
unhook(): void { }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,17 +230,17 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
protected _collect: string;
|
protected _collect: string;
|
||||||
|
|
||||||
// handler lookup containers
|
// 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 _executeHandlers: any;
|
||||||
protected _csiHandlers: any;
|
protected _csiHandlers: IHandlerCollection<CsiHandler>;
|
||||||
protected _escHandlers: any;
|
protected _escHandlers: any;
|
||||||
protected _oscHandlers: any;
|
protected _oscHandlers: IHandlerCollection<OscHandler>;
|
||||||
protected _dcsHandlers: any;
|
protected _dcsHandlers: any;
|
||||||
protected _activeDcsHandler: IDcsHandler | null;
|
protected _activeDcsHandler: IDcsHandler | null;
|
||||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||||
|
|
||||||
// fallback handlers
|
// 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 _executeHandlerFb: (code: number) => void;
|
||||||
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
|
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
|
||||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||||
@@ -261,6 +273,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
this._dcsHandlers = Object.create(null);
|
this._dcsHandlers = Object.create(null);
|
||||||
this._activeDcsHandler = null;
|
this._activeDcsHandler = null;
|
||||||
this._errorHandler = this._errorHandlerFb;
|
this._errorHandler = this._errorHandlerFb;
|
||||||
|
|
||||||
|
// swallow 7bit ST (ESC+\)
|
||||||
|
this.setEscHandler('\\', () => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
@@ -273,15 +288,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
this._errorHandlerFb = null;
|
this._errorHandlerFb = null;
|
||||||
this._printHandler = null;
|
this._printHandler = null;
|
||||||
this._executeHandlers = null;
|
this._executeHandlers = null;
|
||||||
this._csiHandlers = null;
|
|
||||||
this._escHandlers = null;
|
this._escHandlers = null;
|
||||||
|
this._csiHandlers = null;
|
||||||
this._oscHandlers = null;
|
this._oscHandlers = null;
|
||||||
this._dcsHandlers = null;
|
this._dcsHandlers = null;
|
||||||
this._activeDcsHandler = null;
|
this._activeDcsHandler = null;
|
||||||
this._errorHandler = 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;
|
this._printHandler = callback;
|
||||||
}
|
}
|
||||||
clearPrintHandler(): void {
|
clearPrintHandler(): void {
|
||||||
@@ -298,8 +313,24 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
this._executeHandlerFb = callback;
|
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 {
|
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 {
|
clearCsiHandler(flag: string): void {
|
||||||
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
||||||
@@ -318,8 +349,23 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
this._escHandlerFb = callback;
|
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 {
|
setOscHandler(ident: number, callback: (data: string) => void): void {
|
||||||
this._oscHandlers[ident] = callback;
|
this._oscHandlers[ident] = [callback];
|
||||||
}
|
}
|
||||||
clearOscHandler(ident: number): void {
|
clearOscHandler(ident: number): void {
|
||||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||||
@@ -353,7 +399,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
this._activeDcsHandler = null;
|
this._activeDcsHandler = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
parse(data: string): void {
|
parse(data: Uint32Array, length: number): void {
|
||||||
let code = 0;
|
let code = 0;
|
||||||
let transition = 0;
|
let transition = 0;
|
||||||
let error = false;
|
let error = false;
|
||||||
@@ -368,15 +414,14 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
let callback: Function | null = null;
|
let callback: Function | null = null;
|
||||||
|
|
||||||
// process input string
|
// process input string
|
||||||
const l = data.length;
|
for (let i = 0; i < length; ++i) {
|
||||||
for (let i = 0; i < l; ++i) {
|
code = data[i];
|
||||||
code = data.charCodeAt(i);
|
|
||||||
|
|
||||||
// shortcut for most chars (print action)
|
// shortcut for most chars (print action)
|
||||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||||
print = (~print) ? print : i;
|
print = (~print) ? print : i;
|
||||||
do code = data.charCodeAt(++i);
|
do i++;
|
||||||
while (i < l && code > 0x1f && code < 0x80);
|
while (i < length && data[i] > 0x1f && data[i] < 0x80);
|
||||||
i--;
|
i--;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -388,7 +433,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
}
|
}
|
||||||
|
|
||||||
// normal transition & action lookup
|
// 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) {
|
switch (transition >> 4) {
|
||||||
case ParserAction.PRINT:
|
case ParserAction.PRINT:
|
||||||
print = (~print) ? print : i;
|
print = (~print) ? print : i;
|
||||||
@@ -420,10 +465,6 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
case ParserState.GROUND:
|
case ParserState.GROUND:
|
||||||
print = (~print) ? print : i;
|
print = (~print) ? print : i;
|
||||||
break;
|
break;
|
||||||
case ParserState.OSC_STRING:
|
|
||||||
osc += String.fromCharCode(code);
|
|
||||||
transition |= ParserState.OSC_STRING;
|
|
||||||
break;
|
|
||||||
case ParserState.CSI_IGNORE:
|
case ParserState.CSI_IGNORE:
|
||||||
transition |= ParserState.CSI_IGNORE;
|
transition |= ParserState.CSI_IGNORE;
|
||||||
break;
|
break;
|
||||||
@@ -460,9 +501,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ParserAction.CSI_DISPATCH:
|
case ParserAction.CSI_DISPATCH:
|
||||||
callback = this._csiHandlers[code];
|
// Trigger CSI Handler
|
||||||
if (callback) callback(params, collect);
|
const handlers = this._csiHandlers[code];
|
||||||
else this._csiHandlerFb(collect, params, 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;
|
break;
|
||||||
case ParserAction.PARAM:
|
case ParserAction.PARAM:
|
||||||
if (code === 0x3b) params.push(0);
|
if (code === 0x3b) params.push(0);
|
||||||
@@ -514,7 +564,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
osc = '';
|
osc = '';
|
||||||
break;
|
break;
|
||||||
case ParserAction.OSC_PUT:
|
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;
|
break;
|
||||||
case ParserAction.OSC_END:
|
case ParserAction.OSC_END:
|
||||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||||
@@ -529,9 +587,18 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
// or with an explicit NaN OSC handler
|
// or with an explicit NaN OSC handler
|
||||||
const identifier = parseInt(osc.substring(0, idx));
|
const identifier = parseInt(osc.substring(0, idx));
|
||||||
const content = osc.substring(idx + 1);
|
const content = osc.substring(idx + 1);
|
||||||
callback = this._oscHandlers[identifier];
|
// Trigger OSC Handler
|
||||||
if (callback) callback(content);
|
const handlers = this._oscHandlers[identifier];
|
||||||
else this._oscHandlerFb(identifier, content);
|
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;
|
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||||
@@ -546,9 +613,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
|||||||
|
|
||||||
// push leftover pushable buffers to terminal
|
// push leftover pushable buffers to terminal
|
||||||
if (currentState === ParserState.GROUND && ~print) {
|
if (currentState === ParserState.GROUND && ~print) {
|
||||||
this._printHandler(data, print, data.length);
|
this._printHandler(data, print, length);
|
||||||
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
|
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
|
||||||
dcsHandler.put(data, dcs, data.length);
|
dcsHandler.put(data, dcs, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save non pushable buffers
|
// 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}]`);
|
||||||
|
}
|
||||||
+463
-6
@@ -3,31 +3,34 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import { InputHandler } from './InputHandler';
|
import { InputHandler } from './InputHandler';
|
||||||
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
|
||||||
|
import { Terminal } from './Terminal';
|
||||||
|
import { IBufferLine } from './core/Types';
|
||||||
|
import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
describe('InputHandler', () => {
|
describe('InputHandler', () => {
|
||||||
describe('save and restore cursor', () => {
|
describe('save and restore cursor', () => {
|
||||||
const terminal = new MockInputHandlingTerminal();
|
const terminal = new MockInputHandlingTerminal();
|
||||||
terminal.buffer.x = 1;
|
terminal.buffer.x = 1;
|
||||||
terminal.buffer.y = 2;
|
terminal.buffer.y = 2;
|
||||||
terminal.curAttr = 3;
|
terminal.curAttrData.fg = 3;
|
||||||
const inputHandler = new InputHandler(terminal);
|
const inputHandler = new InputHandler(terminal);
|
||||||
// Save cursor position
|
// Save cursor position
|
||||||
inputHandler.saveCursor([]);
|
inputHandler.saveCursor([]);
|
||||||
assert.equal(terminal.buffer.x, 1);
|
assert.equal(terminal.buffer.x, 1);
|
||||||
assert.equal(terminal.buffer.y, 2);
|
assert.equal(terminal.buffer.y, 2);
|
||||||
assert.equal(terminal.curAttr, 3);
|
assert.equal(terminal.curAttrData.fg, 3);
|
||||||
// Change cursor position
|
// Change cursor position
|
||||||
terminal.buffer.x = 10;
|
terminal.buffer.x = 10;
|
||||||
terminal.buffer.y = 20;
|
terminal.buffer.y = 20;
|
||||||
terminal.curAttr = 30;
|
terminal.curAttrData.fg = 30;
|
||||||
// Restore cursor position
|
// Restore cursor position
|
||||||
inputHandler.restoreCursor([]);
|
inputHandler.restoreCursor([]);
|
||||||
assert.equal(terminal.buffer.x, 1);
|
assert.equal(terminal.buffer.x, 1);
|
||||||
assert.equal(terminal.buffer.y, 2);
|
assert.equal(terminal.buffer.y, 2);
|
||||||
assert.equal(terminal.curAttr, 3);
|
assert.equal(terminal.curAttrData.fg, 3);
|
||||||
});
|
});
|
||||||
describe('setCursorStyle', () => {
|
describe('setCursorStyle', () => {
|
||||||
it('should call Terminal.setOption with correct params', () => {
|
it('should call Terminal.setOption with correct params', () => {
|
||||||
@@ -84,4 +87,458 @@ describe('InputHandler', () => {
|
|||||||
assert.equal(terminal.bracketedPasteMode, false);
|
assert.equal(terminal.bracketedPasteMode, false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('regression tests', function(): void {
|
||||||
|
function termContent(term: Terminal, trim: boolean): string[] {
|
||||||
|
const result = [];
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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;
|
||||||
|
inputHandler.insertChars([0]);
|
||||||
|
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;
|
||||||
|
inputHandler.insertChars([1]);
|
||||||
|
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;
|
||||||
|
inputHandler.insertChars([2]);
|
||||||
|
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;
|
||||||
|
inputHandler.insertChars([10]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
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;
|
||||||
|
inputHandler.deleteChars([0]);
|
||||||
|
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;
|
||||||
|
inputHandler.deleteChars([1]);
|
||||||
|
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;
|
||||||
|
inputHandler.deleteChars([2]);
|
||||||
|
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;
|
||||||
|
inputHandler.deleteChars([10]);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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'));
|
||||||
|
|
||||||
|
// params[0] - right erase
|
||||||
|
term.buffer.y = 0;
|
||||||
|
term.buffer.x = 70;
|
||||||
|
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([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;
|
||||||
|
inputHandler.eraseInLine([2]);
|
||||||
|
expect(term.buffer.lines.get(2).translateToString(false)).equals(Array(term.cols + 1).join(' '));
|
||||||
|
|
||||||
|
});
|
||||||
|
it('eraseInDisplay', function(): void {
|
||||||
|
const term = new Terminal({cols: 80, rows: 7});
|
||||||
|
const inputHandler = new InputHandler(term);
|
||||||
|
|
||||||
|
// fill display with a's
|
||||||
|
for (let i = 0; i < term.rows; ++i) inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||||
|
|
||||||
|
// params [0] - right and below erase
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
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(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
|
||||||
|
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(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
|
||||||
|
const termNotConverting = new Terminal({cols: 15, rows: 10});
|
||||||
|
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||||
|
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
|
||||||
|
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
|
||||||
|
(termConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||||
|
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);
|
||||||
|
const container = new Uint32Array(10);
|
||||||
|
container[0] = 0x200B;
|
||||||
|
inputHandler.print(container, 0, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('alt screen', () => {
|
||||||
|
let term: Terminal;
|
||||||
|
let handler: InputHandler;
|
||||||
|
|
||||||
|
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(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).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(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).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(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).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||||
|
// Text color of 'JUNK' should be red
|
||||||
|
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(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).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(term.buffer.translateBufferLineToString(0, true)).to.equal('TEST');
|
||||||
|
// Text color of 'TEST' should be default
|
||||||
|
expect(term.buffer.lines.get(0).loadCell(0, new CellData()).fg).to.equal(DEFAULT_ATTR_DATA.fg);
|
||||||
|
handler.parse('\x1b[?1049h\x1b[uTEST');
|
||||||
|
expect(term.buffer.translateBufferLineToString(1, true)).to.equal('TEST');
|
||||||
|
// Text color of 'TEST' should be red
|
||||||
|
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).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]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+347
-291
File diff suppressed because it is too large
Load Diff
+109
-12
@@ -4,11 +4,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types';
|
||||||
import { ILinkMatcher, LineData, ITerminal } from './Types';
|
import { IBufferLine } from './core/Types';
|
||||||
import { Linkifier } from './Linkifier';
|
import { Linkifier } from './Linkifier';
|
||||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
|
||||||
import { CircularList } from './common/CircularList';
|
import { CircularList } from './common/CircularList';
|
||||||
|
import { BufferLine, CellData } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
class TestLinkifier extends Linkifier {
|
class TestLinkifier extends Linkifier {
|
||||||
constructor(terminal: ITerminal) {
|
constructor(terminal: ITerminal) {
|
||||||
@@ -40,18 +41,19 @@ describe('Linkifier', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
terminal = new MockTerminal();
|
terminal = new MockTerminal();
|
||||||
terminal.cols = 100;
|
(terminal as any).cols = 100;
|
||||||
|
(terminal as any).rows = 10;
|
||||||
terminal.buffer = new MockBuffer();
|
terminal.buffer = new MockBuffer();
|
||||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
|
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||||
terminal.buffer.ydisp = 0;
|
terminal.buffer.ydisp = 0;
|
||||||
linkifier = new TestLinkifier(terminal);
|
linkifier = new TestLinkifier(terminal);
|
||||||
mouseZoneManager = new TestMouseZoneManager();
|
mouseZoneManager = new TestMouseZoneManager();
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringToRow(text: string): LineData {
|
function stringToRow(text: string): IBufferLine {
|
||||||
const result: LineData = [];
|
const result = new BufferLine(text.length);
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,7 @@ describe('Linkifier', () => {
|
|||||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||||
addRow(rowText);
|
addRow(rowText);
|
||||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||||
|
(terminal as any).rows = terminal.buffer.lines.length - 1;
|
||||||
linkifier.linkifyRows();
|
linkifier.linkifyRows();
|
||||||
// Allow linkify to happen
|
// Allow linkify to happen
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -139,19 +142,19 @@ describe('Linkifier', () => {
|
|||||||
});
|
});
|
||||||
describe('multi-line links', () => {
|
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 => {
|
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);
|
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 => {
|
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);
|
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 => {
|
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);
|
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 => {
|
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);
|
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -237,4 +240,98 @@ describe('Linkifier', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
describe('unicode handling', () => {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
|
||||||
|
// other than the tests above unicode testing needs the full terminal instance
|
||||||
|
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
|
||||||
|
beforeEach(() => {
|
||||||
|
terminal = new TestTerminal({cols: 10, rows: 5});
|
||||||
|
linkifier = new TestLinkifier(terminal);
|
||||||
|
mouseZoneManager = new TestMouseZoneManager();
|
||||||
|
linkifier.attachToDom(mouseZoneManager);
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||||
|
terminal.writeSync(rowText);
|
||||||
|
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||||
|
linkifier.linkifyRows();
|
||||||
|
// Allow linkify to happen
|
||||||
|
setTimeout(() => {
|
||||||
|
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||||
|
links.forEach((l, i) => {
|
||||||
|
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
|
||||||
|
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('unicode before the match', () => {
|
||||||
|
it('combining - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('unicode within the match', () => {
|
||||||
|
it('combining - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||||
|
});
|
||||||
|
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||||
|
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+117
-88
@@ -3,15 +3,15 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult, IMouseZoneManager } from './Types';
|
||||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
|
import { MouseZone } from './MouseZoneManager';
|
||||||
import { MouseZone } from './ui/MouseZoneManager';
|
import { getStringCellWidth } from './CharWidth';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter2, IEvent } from './common/EventEmitter2';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
* 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 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
|
* the costly operation of searching every row multiple times, potentially a
|
||||||
@@ -19,17 +19,30 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
*/
|
*/
|
||||||
protected static readonly TIME_BEFORE_LINKIFY = 200;
|
protected static readonly TIME_BEFORE_LINKIFY = 200;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limit of the unwrapping line expansion (overscan) at the top and bottom
|
||||||
|
* of the actual viewport in ASCII characters.
|
||||||
|
* A limit of 2000 should match most sane urls.
|
||||||
|
*/
|
||||||
|
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
|
||||||
|
|
||||||
protected _linkMatchers: ILinkMatcher[] = [];
|
protected _linkMatchers: ILinkMatcher[] = [];
|
||||||
|
|
||||||
private _mouseZoneManager: IMouseZoneManager;
|
private _mouseZoneManager: IMouseZoneManager;
|
||||||
private _rowsTimeoutId: number;
|
private _rowsTimeoutId: number;
|
||||||
private _nextLinkMatcherId = 0;
|
private _nextLinkMatcherId = 0;
|
||||||
private _rowsToLinkify: {start: number, end: number};
|
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(
|
constructor(
|
||||||
protected _terminal: ITerminal
|
protected _terminal: ITerminal
|
||||||
) {
|
) {
|
||||||
super();
|
|
||||||
this._rowsToLinkify = {
|
this._rowsToLinkify = {
|
||||||
start: null,
|
start: null,
|
||||||
end: null
|
end: null
|
||||||
@@ -79,9 +92,37 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
*/
|
*/
|
||||||
private _linkifyRows(): void {
|
private _linkifyRows(): void {
|
||||||
this._rowsTimeoutId = null;
|
this._rowsTimeoutId = null;
|
||||||
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
|
const buffer = this._terminal.buffer;
|
||||||
this._linkifyRow(i);
|
|
||||||
|
// Ensure the start row exists
|
||||||
|
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
|
||||||
|
if (absoluteRowIndexStart >= buffer.lines.length) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Invalidate bad end row values (if a resize happened)
|
||||||
|
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
|
||||||
|
|
||||||
|
// Iterate over the range of unwrapped content strings within start..end
|
||||||
|
// (excluding).
|
||||||
|
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
|
||||||
|
// for every matcher.
|
||||||
|
// The unwrapping is needed to also match content that got wrapped across
|
||||||
|
// several buffer lines. To avoid a worst case scenario where the whole buffer
|
||||||
|
// contains just a single unwrapped string we limit this line expansion beyond
|
||||||
|
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
|
||||||
|
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
|
||||||
|
// chars will not match anymore at the viewport borders.
|
||||||
|
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
|
||||||
|
const iterator = this._terminal.buffer.iterator(
|
||||||
|
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
const lineData: IBufferStringIteratorResult = iterator.next();
|
||||||
|
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||||
|
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this._rowsToLinkify.start = null;
|
this._rowsToLinkify.start = null;
|
||||||
this._rowsToLinkify.end = null;
|
this._rowsToLinkify.end = null;
|
||||||
}
|
}
|
||||||
@@ -152,82 +193,68 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Linkifies a row.
|
|
||||||
* @param rowIndex The index of the row to linkify.
|
|
||||||
*/
|
|
||||||
private _linkifyRow(rowIndex: number): void {
|
|
||||||
// Ensure the row exists
|
|
||||||
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
|
||||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped) {
|
|
||||||
// Only attempt to linkify rows that start in the viewport
|
|
||||||
if (rowIndex !== 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
|
||||||
do {
|
|
||||||
rowIndex--;
|
|
||||||
absoluteRowIndex--;
|
|
||||||
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).isWrapped);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct full unwrapped line text
|
|
||||||
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
|
||||||
let currentIndex = absoluteRowIndex + 1;
|
|
||||||
while (currentIndex < this._terminal.buffer.lines.length &&
|
|
||||||
(<any>this._terminal.buffer.lines.get(currentIndex)).isWrapped) {
|
|
||||||
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
|
||||||
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Linkifies a row given a specific handler.
|
* Linkifies a row given a specific handler.
|
||||||
* @param rowIndex The row index to linkify.
|
* @param rowIndex The row index to linkify (absolute index).
|
||||||
* @param text The text of the row (excludes text in the row that's already
|
* @param text string content of the unwrapped row.
|
||||||
* linkified).
|
|
||||||
* @param matcher The link matcher for this line.
|
* @param matcher The link matcher for this line.
|
||||||
* @param offset The how much of the row has already been linkified.
|
|
||||||
* @return The link element(s) that were added.
|
|
||||||
*/
|
*/
|
||||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
|
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
|
||||||
// Find the first match
|
// clone regex to do a global search on text
|
||||||
const match = text.match(matcher.regex);
|
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
|
||||||
if (!match || match.length === 0) {
|
let match;
|
||||||
return;
|
let stringIndex = -1;
|
||||||
}
|
while ((match = rex.exec(text)) !== null) {
|
||||||
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||||
|
if (!uri) {
|
||||||
// Get index, match.index is for the outer match which includes negated chars
|
// something matched but does not comply with the given matchIndex
|
||||||
const index = text.indexOf(uri);
|
// since this is most likely a bug the regex itself we simply do nothing here
|
||||||
|
// DEBUG: print match and throw
|
||||||
// Ensure the link is valid before registering
|
if ((<any>this._terminal).debug) {
|
||||||
if (matcher.validationCallback) {
|
console.log({match, matcher});
|
||||||
matcher.validationCallback(uri, isValid => {
|
throw new Error('match found without corresponding matchIndex');
|
||||||
// Discard link if the line has already changed
|
|
||||||
if (this._rowsTimeoutId) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (isValid) {
|
break;
|
||||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively check for links in the rest of the text
|
// Get index, match.index is for the outer match which includes negated chars
|
||||||
const remainingStartIndex = index + uri.length;
|
// therefore we cannot use match.index directly, instead we search the position
|
||||||
const remainingText = text.substr(remainingStartIndex);
|
// of the match group in text again
|
||||||
if (remainingText.length > 0) {
|
// also correct regex and string search offsets for the next loop run
|
||||||
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
|
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 attr = line.getFg(bufferIndex[1]);
|
||||||
|
let fg: number | undefined;
|
||||||
|
if (attr) {
|
||||||
|
fg = (attr >> 9) & 0x1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matcher.validationCallback) {
|
||||||
|
matcher.validationCallback(uri, isValid => {
|
||||||
|
// Discard link if the line has already changed
|
||||||
|
if (this._rowsTimeoutId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isValid) {
|
||||||
|
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,12 +264,14 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
* @param y The row the link is on.
|
* @param y The row the link is on.
|
||||||
* @param uri The URI of the link.
|
* @param uri The URI of the link.
|
||||||
* @param matcher The link matcher for the link.
|
* @param matcher The link matcher for the link.
|
||||||
|
* @param fg The link color for hover event.
|
||||||
*/
|
*/
|
||||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
|
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
|
||||||
|
const width = getStringCellWidth(uri);
|
||||||
const x1 = x % this._terminal.cols;
|
const x1 = x % this._terminal.cols;
|
||||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||||
let x2 = (x1 + uri.length) % this._terminal.cols;
|
let x2 = (x1 + width) % this._terminal.cols;
|
||||||
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
|
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
|
||||||
if (x2 === 0) {
|
if (x2 === 0) {
|
||||||
x2 = this._terminal.cols;
|
x2 = this._terminal.cols;
|
||||||
y2--;
|
y2--;
|
||||||
@@ -259,18 +288,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
}
|
}
|
||||||
window.open(uri, '_blank');
|
window.open(uri, '_blank');
|
||||||
},
|
},
|
||||||
e => {
|
() => {
|
||||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this._onLinkHover.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||||
},
|
},
|
||||||
e => {
|
e => {
|
||||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
if (matcher.hoverTooltipCallback) {
|
if (matcher.hoverTooltipCallback) {
|
||||||
matcher.hoverTooltipCallback(e, uri);
|
matcher.hoverTooltipCallback(e, uri);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
() => {
|
() => {
|
||||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
|
this._onLinkLeave.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||||
if (matcher.hoverLeaveCallback) {
|
if (matcher.hoverLeaveCallback) {
|
||||||
matcher.hoverLeaveCallback();
|
matcher.hoverLeaveCallback();
|
||||||
@@ -285,7 +314,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
|
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkifierEvent {
|
||||||
return { x1, y1, x2, y2, cols: this._terminal.cols };
|
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,40 +31,34 @@ describe('MouseHelper.getCoords', () => {
|
|||||||
actualCellWidth: CHAR_WIDTH,
|
actualCellWidth: CHAR_WIDTH,
|
||||||
actualCellHeight: CHAR_HEIGHT
|
actualCellHeight: CHAR_HEIGHT
|
||||||
};
|
};
|
||||||
mouseHelper = new MouseHelper(renderer);
|
mouseHelper = new MouseHelper(renderer as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when charMeasure is not initialized', () => {
|
describe('when charMeasure is not initialized', () => {
|
||||||
it('should return null', () => {
|
it('should return null', () => {
|
||||||
charMeasure = new MockCharMeasure();
|
charMeasure = new MockCharMeasure();
|
||||||
assert.equal(mouseHelper.getCoords({ pageX: 0, pageY: 0 }, document.createElement('div'), charMeasure, 1, 10, 10), null);
|
assert.equal(mouseHelper.getCoords({ clientX: 0, clientY: 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, 1, 10, 10), null);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the cell that was clicked', () => {
|
it('should return the cell that was clicked', () => {
|
||||||
let coords: [number, number];
|
let coords: [number, number];
|
||||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH / 2, pageY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH / 2, clientY: CHAR_HEIGHT / 2 }, document.createElement('div'), charMeasure, 10, 10);
|
||||||
assert.deepEqual(coords, [1, 1]);
|
assert.deepEqual(coords, [1, 1]);
|
||||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||||
assert.deepEqual(coords, [1, 1]);
|
assert.deepEqual(coords, [1, 1]);
|
||||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH, pageY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH, clientY: CHAR_HEIGHT + 1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||||
assert.deepEqual(coords, [1, 2]);
|
assert.deepEqual(coords, [1, 2]);
|
||||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH + 1, pageY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = mouseHelper.getCoords({ clientX: CHAR_WIDTH + 1, clientY: CHAR_HEIGHT }, document.createElement('div'), charMeasure, 10, 10);
|
||||||
assert.deepEqual(coords, [2, 1]);
|
assert.deepEqual(coords, [2, 1]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
it('should ensure the coordinates are returned within the terminal bounds', () => {
|
||||||
let coords: [number, number];
|
let coords: [number, number];
|
||||||
coords = mouseHelper.getCoords({ pageX: -1, pageY: -1 }, document.createElement('div'), charMeasure, 1, 10, 10);
|
coords = mouseHelper.getCoords({ clientX: -1, clientY: -1 }, document.createElement('div'), charMeasure, 10, 10);
|
||||||
assert.deepEqual(coords, [1, 1]);
|
assert.deepEqual(coords, [1, 1]);
|
||||||
// Event are double the cols/rows
|
// Event are double the cols/rows
|
||||||
coords = mouseHelper.getCoords({ pageX: CHAR_WIDTH * 20, pageY: CHAR_HEIGHT * 20 }, document.createElement('div'), charMeasure, 1, 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');
|
assert.deepEqual(coords, [10, 10], 'coordinates should never come back as larger than the terminal');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3,36 +3,18 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICharMeasure } from '../Types';
|
import { ICharMeasure, IMouseHelper } from './Types';
|
||||||
import { IRenderer } from '../renderer/Types';
|
import { RenderCoordinator } from './renderer/RenderCoordinator';
|
||||||
|
|
||||||
export class MouseHelper {
|
export class MouseHelper implements IMouseHelper {
|
||||||
constructor(private _renderer: IRenderer) {}
|
constructor(
|
||||||
|
private _renderCoordinator: RenderCoordinator
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
public static getCoordsRelativeToElement(event: {pageX: number, pageY: number}, element: HTMLElement): [number, number] {
|
public static getCoordsRelativeToElement(event: {clientX: number, clientY: number}, element: HTMLElement): [number, number] {
|
||||||
// Ignore browsers that don't support MouseEvent.pageX
|
const rect = element.getBoundingClientRect();
|
||||||
if (event.pageX == null) {
|
return [event.clientX - rect.left, event.clientY - rect.top];
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +30,7 @@ export class MouseHelper {
|
|||||||
* apply an offset to the x value such that the left half of the cell will
|
* 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.
|
* select that cell and the right half will select the next cell.
|
||||||
*/
|
*/
|
||||||
public getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, 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
|
// Coordinates cannot be measured if charMeasure has not been initialized
|
||||||
if (!charMeasure.width || !charMeasure.height) {
|
if (!charMeasure.width || !charMeasure.height) {
|
||||||
return null;
|
return null;
|
||||||
@@ -59,8 +41,8 @@ export class MouseHelper {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderer.dimensions.actualCellWidth / 2 : 0)) / this._renderer.dimensions.actualCellWidth);
|
coords[0] = Math.ceil((coords[0] + (isSelection ? this._renderCoordinator.dimensions.actualCellWidth / 2 : 0)) / this._renderCoordinator.dimensions.actualCellWidth);
|
||||||
coords[1] = Math.ceil(coords[1] / this._renderer.dimensions.actualCellHeight);
|
coords[1] = Math.ceil(coords[1] / this._renderCoordinator.dimensions.actualCellHeight);
|
||||||
|
|
||||||
// Ensure coordinates are within the terminal viewport. Note that selections
|
// Ensure coordinates are within the terminal viewport. Note that selections
|
||||||
// need an addition point of precision to cover the end point (as characters
|
// need an addition point of precision to cover the end point (as characters
|
||||||
@@ -81,8 +63,8 @@ export class MouseHelper {
|
|||||||
* @param colCount The number of columns in the terminal.
|
* @param colCount The number of columns in the terminal.
|
||||||
* @param rowCount The number of rows in the terminal.
|
* @param rowCount The number of rows in the terminal.
|
||||||
*/
|
*/
|
||||||
public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number } {
|
public getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number } {
|
||||||
const coords = this.getCoords(event, element, charMeasure, lineHeight, colCount, rowCount);
|
const coords = this.getCoords(event, element, charMeasure, colCount, rowCount);
|
||||||
let x = coords[0];
|
let x = coords[0];
|
||||||
let y = coords[1];
|
let y = coords[1];
|
||||||
|
|
||||||
@@ -3,10 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal } from '../Types';
|
import { ITerminal, IMouseZoneManager, IMouseZone } from './Types';
|
||||||
import { IMouseZoneManager, IMouseZone } from './Types';
|
import { Disposable } from './common/Lifecycle';
|
||||||
import { Disposable } from '../common/Lifecycle';
|
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||||
import { addDisposableDomListener } from './Lifecycle';
|
|
||||||
|
|
||||||
const HOVER_DURATION = 500;
|
const HOVER_DURATION = 500;
|
||||||
|
|
||||||
@@ -23,11 +22,13 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
|||||||
|
|
||||||
private _areZonesActive: boolean = false;
|
private _areZonesActive: boolean = false;
|
||||||
private _mouseMoveListener: (e: MouseEvent) => any;
|
private _mouseMoveListener: (e: MouseEvent) => any;
|
||||||
|
private _mouseLeaveListener: (e: MouseEvent) => any;
|
||||||
private _clickListener: (e: MouseEvent) => any;
|
private _clickListener: (e: MouseEvent) => any;
|
||||||
|
|
||||||
private _tooltipTimeout: number = null;
|
private _tooltipTimeout: number = null;
|
||||||
private _currentZone: IMouseZone = null;
|
private _currentZone: IMouseZone = null;
|
||||||
private _lastHoverCoords: [number, number] = [null, null];
|
private _lastHoverCoords: [number, number] = [null, null];
|
||||||
|
private _initialSelectionLength: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _terminal: ITerminal
|
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
|
// These events are expensive, only listen to it when mouse zones are active
|
||||||
this._mouseMoveListener = e => this._onMouseMove(e);
|
this._mouseMoveListener = e => this._onMouseMove(e);
|
||||||
|
this._mouseLeaveListener = e => this._onMouseLeave(e);
|
||||||
this._clickListener = e => this._onClick(e);
|
this._clickListener = e => this._onClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
|||||||
if (!this._areZonesActive) {
|
if (!this._areZonesActive) {
|
||||||
this._areZonesActive = true;
|
this._areZonesActive = true;
|
||||||
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
|
this._terminal.element.addEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._terminal.element.addEventListener('mouseleave', this._mouseLeaveListener);
|
||||||
this._terminal.element.addEventListener('click', this._clickListener);
|
this._terminal.element.addEventListener('click', this._clickListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,6 +100,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
|||||||
if (this._areZonesActive) {
|
if (this._areZonesActive) {
|
||||||
this._areZonesActive = false;
|
this._areZonesActive = false;
|
||||||
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
|
this._terminal.element.removeEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._terminal.element.removeEventListener('mouseleave', this._mouseLeaveListener);
|
||||||
this._terminal.element.removeEventListener('click', this._clickListener);
|
this._terminal.element.removeEventListener('click', this._clickListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,6 +157,10 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseDown(e: MouseEvent): void {
|
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
|
// Ignore the event if there are no zones active
|
||||||
if (!this._areZonesActive) {
|
if (!this._areZonesActive) {
|
||||||
return;
|
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 {
|
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);
|
const zone = this._findZoneEventAt(e);
|
||||||
if (zone) {
|
const currentSelectionLength = this._terminal.getSelection().length;
|
||||||
|
|
||||||
|
if (zone && currentSelectionLength === this._initialSelectionLength) {
|
||||||
zone.clickCallback(e);
|
zone.clickCallback(e);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopImmediatePropagation();
|
e.stopImmediatePropagation();
|
||||||
@@ -180,7 +203,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _findZoneEventAt(e: MouseEvent): IMouseZone {
|
private _findZoneEventAt(e: MouseEvent): IMouseZone {
|
||||||
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows);
|
const coords = this._terminal.mouseHelper.getCoords(e, this._terminal.screenElement, this._terminal.charMeasure, this._terminal.cols, this._terminal.rows);
|
||||||
if (!coords) {
|
if (!coords) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -4,12 +4,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { CharMeasure } from './ui/CharMeasure';
|
import { CharMeasure } from './CharMeasure';
|
||||||
import { SelectionManager, SelectionMode } from './SelectionManager';
|
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
import { ITerminal, IBuffer } from './Types';
|
||||||
import { MockTerminal } from './utils/TestUtils.test';
|
import { IBufferLine } from './core/Types';
|
||||||
|
import { MockTerminal } from './TestUtils.test';
|
||||||
|
import { BufferLine, CellData } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
class TestMockTerminal extends MockTerminal {
|
class TestMockTerminal extends MockTerminal {
|
||||||
emit(event: string, data: any): void {}
|
emit(event: string, data: any): void {}
|
||||||
@@ -29,6 +31,7 @@ class TestSelectionManager extends SelectionManager {
|
|||||||
|
|
||||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||||
|
public areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean { return this._areCoordsInSelection(coords, start, end); }
|
||||||
|
|
||||||
// Disable DOM interaction
|
// Disable DOM interaction
|
||||||
public enable(): void {}
|
public enable(): void {}
|
||||||
@@ -43,8 +46,8 @@ describe('SelectionManager', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
terminal = new TestMockTerminal();
|
terminal = new TestMockTerminal();
|
||||||
terminal.cols = 80;
|
(terminal as any).cols = 80;
|
||||||
terminal.rows = 2;
|
(terminal as any).rows = 2;
|
||||||
terminal.options.scrollback = 100;
|
terminal.options.scrollback = 100;
|
||||||
terminal.buffers = new BufferSet(terminal);
|
terminal.buffers = new BufferSet(terminal);
|
||||||
terminal.buffer = terminal.buffers.active;
|
terminal.buffer = terminal.buffers.active;
|
||||||
@@ -52,16 +55,18 @@ describe('SelectionManager', () => {
|
|||||||
selectionManager = new TestSelectionManager(terminal, null);
|
selectionManager = new TestSelectionManager(terminal, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
function stringToRow(text: string): LineData {
|
function stringToRow(text: string): IBufferLine {
|
||||||
const result: LineData = [];
|
const result = new BufferLine(text.length);
|
||||||
for (let i = 0; i < text.length; i++) {
|
for (let i = 0; i < text.length; i++) {
|
||||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)]));
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringArrayToRow(chars: string[]): LineData {
|
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||||
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
|
const line = new BufferLine(chars.length);
|
||||||
|
chars.map((c, idx) => line.setCell(idx, CellData.fromCharData([0, c, 1, c.charCodeAt(0)])));
|
||||||
|
return line;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('_selectWordAt', () => {
|
describe('_selectWordAt', () => {
|
||||||
@@ -97,7 +102,7 @@ describe('SelectionManager', () => {
|
|||||||
});
|
});
|
||||||
it('should expand selection for wide characters', () => {
|
it('should expand selection for wide characters', () => {
|
||||||
// Wide characters use a special format
|
// Wide characters use a special format
|
||||||
buffer.lines.set(0, [
|
const data: [number, string, number, number][] = [
|
||||||
[null, '中', 2, '中'.charCodeAt(0)],
|
[null, '中', 2, '中'.charCodeAt(0)],
|
||||||
[null, '', 0, null],
|
[null, '', 0, null],
|
||||||
[null, '文', 2, '文'.charCodeAt(0)],
|
[null, '文', 2, '文'.charCodeAt(0)],
|
||||||
@@ -113,7 +118,10 @@ describe('SelectionManager', () => {
|
|||||||
[null, 'f', 1, 'f'.charCodeAt(0)],
|
[null, 'f', 1, 'f'.charCodeAt(0)],
|
||||||
[null, 'o', 1, 'o'.charCodeAt(0)],
|
[null, 'o', 1, 'o'.charCodeAt(0)],
|
||||||
[null, 'o', 1, 'o'.charCodeAt(0)]
|
[null, 'o', 1, 'o'.charCodeAt(0)]
|
||||||
]);
|
];
|
||||||
|
const line = new BufferLine(data.length);
|
||||||
|
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
|
// Ensure wide characters take up 2 columns
|
||||||
selectionManager.selectWordAt([0, 0]);
|
selectionManager.selectWordAt([0, 0]);
|
||||||
assert.equal(selectionManager.selectionText, '中文');
|
assert.equal(selectionManager.selectionText, '中文');
|
||||||
@@ -183,6 +191,42 @@ describe('SelectionManager', () => {
|
|||||||
selectionManager.selectWordAt([15, 0]);
|
selectionManager.selectWordAt([15, 0]);
|
||||||
assert.equal(selectionManager.selectionText, 'ij"');
|
assert.equal(selectionManager.selectionText, 'ij"');
|
||||||
});
|
});
|
||||||
|
it('should expand upwards or downards for wrapped lines', () => {
|
||||||
|
buffer.lines.set(0, stringToRow(' foo'));
|
||||||
|
buffer.lines.set(1, stringToRow('bar '));
|
||||||
|
buffer.lines.get(1).isWrapped = true;
|
||||||
|
selectionManager.selectWordAt([1, 1]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foobar');
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([78, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, 'foobar');
|
||||||
|
});
|
||||||
|
it('should expand both upwards and downwards for word wrapped over many lines', () => {
|
||||||
|
const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar';
|
||||||
|
buffer.lines.set(0, stringToRow(' foo'));
|
||||||
|
buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
||||||
|
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
||||||
|
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
||||||
|
buffer.lines.set(4, stringToRow('bar '));
|
||||||
|
buffer.lines.get(1).isWrapped = true;
|
||||||
|
buffer.lines.get(2).isWrapped = true;
|
||||||
|
buffer.lines.get(3).isWrapped = true;
|
||||||
|
buffer.lines.get(4).isWrapped = true;
|
||||||
|
selectionManager.selectWordAt([78, 0]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 1]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 2]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([40, 3]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
selectionManager.model.clearSelection();
|
||||||
|
selectionManager.selectWordAt([1, 4]);
|
||||||
|
assert.equal(selectionManager.selectionText, expectedText);
|
||||||
|
});
|
||||||
describe('emoji', () => {
|
describe('emoji', () => {
|
||||||
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
||||||
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||||
@@ -303,7 +347,7 @@ describe('SelectionManager', () => {
|
|||||||
it('should select the entire wrapped line', () => {
|
it('should select the entire wrapped line', () => {
|
||||||
buffer.lines.set(0, stringToRow('foo'));
|
buffer.lines.set(0, stringToRow('foo'));
|
||||||
const line2 = stringToRow('bar');
|
const line2 = stringToRow('bar');
|
||||||
(<any>line2).isWrapped = true;
|
line2.isWrapped = true;
|
||||||
buffer.lines.set(1, line2);
|
buffer.lines.set(1, line2);
|
||||||
selectionManager.selectLineAt(0);
|
selectionManager.selectLineAt(0);
|
||||||
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
||||||
@@ -436,5 +480,17 @@ describe('SelectionManager', () => {
|
|||||||
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
|
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('_areCoordsInSelection', () => {
|
||||||
|
it('should return whether coords are in the selection', () => {
|
||||||
|
assert.isFalse(selectionManager.areCoordsInSelection([0, 0], [2, 0], [2, 1]));
|
||||||
|
assert.isFalse(selectionManager.areCoordsInSelection([1, 0], [2, 0], [2, 1]));
|
||||||
|
assert.isTrue(selectionManager.areCoordsInSelection([2, 0], [2, 0], [2, 1]));
|
||||||
|
assert.isTrue(selectionManager.areCoordsInSelection([10, 0], [2, 0], [2, 1]));
|
||||||
|
assert.isTrue(selectionManager.areCoordsInSelection([0, 1], [2, 0], [2, 1]));
|
||||||
|
assert.isTrue(selectionManager.areCoordsInSelection([1, 1], [2, 0], [2, 1]));
|
||||||
|
assert.isFalse(selectionManager.areCoordsInSelection([2, 1], [2, 0], [2, 1]));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+154
-74
@@ -3,14 +3,16 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
|
import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types';
|
||||||
import { MouseHelper } from './utils/MouseHelper';
|
import { IBufferLine } from './core/Types';
|
||||||
import * as Browser from './shared/utils/Browser';
|
import { MouseHelper } from './MouseHelper';
|
||||||
import { CharMeasure } from './ui/CharMeasure';
|
import * as Browser from './common/Platform';
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { CharMeasure } from './CharMeasure';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
|
||||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
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
|
* The number of pixels the mouse needs to be above or below the viewport in
|
||||||
@@ -66,10 +68,10 @@ export const enum SelectionMode {
|
|||||||
* SelectionModel, SelectionManager handles with all logic associated with
|
* SelectionModel, SelectionManager handles with all logic associated with
|
||||||
* dealing with the selection, including handling mouse interaction, wide
|
* dealing with the selection, including handling mouse interaction, wide
|
||||||
* characters and fetching the actual text within the selection. Rendering is
|
* characters and fetching the actual text within the selection. Rendering is
|
||||||
* not handled by the SelectionManager but a 'refresh' event is fired when the
|
* not handled by the SelectionManager but the onRedrawRequest event is fired
|
||||||
* selection is ready to be redrawn.
|
* 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;
|
protected _model: SelectionModel;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -101,15 +103,22 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
private _mouseMoveListener: EventListener;
|
private _mouseMoveListener: EventListener;
|
||||||
private _mouseUpListener: EventListener;
|
private _mouseUpListener: EventListener;
|
||||||
private _trimListener: XtermListener;
|
private _trimListener: IDisposable;
|
||||||
|
private _workCell: CellData = new CellData();
|
||||||
|
|
||||||
private _mouseDownTimeStamp: number;
|
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(
|
constructor(
|
||||||
private _terminal: ITerminal,
|
private _terminal: ITerminal,
|
||||||
private _charMeasure: CharMeasure
|
private _charMeasure: CharMeasure
|
||||||
) {
|
) {
|
||||||
super();
|
|
||||||
this._initListeners();
|
this._initListeners();
|
||||||
this.enable();
|
this.enable();
|
||||||
|
|
||||||
@@ -118,7 +127,6 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
super.dispose();
|
|
||||||
this._removeMouseDownListeners();
|
this._removeMouseDownListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,14 +140,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
private _initListeners(): void {
|
private _initListeners(): void {
|
||||||
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
this._mouseMoveListener = event => this._onMouseMove(<MouseEvent>event);
|
||||||
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
this._mouseUpListener = event => this._onMouseUp(<MouseEvent>event);
|
||||||
this._trimListener = (amount: number) => this._onTrim(amount);
|
|
||||||
|
|
||||||
this.initBuffersListeners();
|
this.initBuffersListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
public initBuffersListeners(): void {
|
public initBuffersListeners(): void {
|
||||||
this._terminal.buffer.lines.on('trim', this._trimListener);
|
this._trimListener = this._terminal.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||||
this._terminal.buffers.on('activate', e => this._onBufferActivate(e));
|
this._terminal.buffers.onBufferActivate(e => this._onBufferActivate(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -197,14 +204,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Get first row
|
// 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));
|
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||||
|
|
||||||
// Get middle rows
|
// Get middle rows
|
||||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||||
const bufferLine = this._buffer.lines.get(i);
|
const bufferLine = this._buffer.lines.get(i);
|
||||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||||
if ((<any>bufferLine).isWrapped) {
|
if (bufferLine.isWrapped) {
|
||||||
result[result.length - 1] += lineText;
|
result[result.length - 1] += lineText;
|
||||||
} else {
|
} else {
|
||||||
result.push(lineText);
|
result.push(lineText);
|
||||||
@@ -215,7 +222,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
if (start[1] !== end[1]) {
|
if (start[1] !== end[1]) {
|
||||||
const bufferLine = this._buffer.lines.get(end[1]);
|
const bufferLine = this._buffer.lines.get(end[1]);
|
||||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||||
if ((<any>bufferLine).isWrapped) {
|
if (bufferLine.isWrapped) {
|
||||||
result[result.length - 1] += lineText;
|
result[result.length - 1] += lineText;
|
||||||
} else {
|
} else {
|
||||||
result.push(lineText);
|
result.push(lineText);
|
||||||
@@ -239,14 +246,15 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
this._model.clearSelection();
|
this._model.clearSelection();
|
||||||
this._removeMouseDownListeners();
|
this._removeMouseDownListeners();
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
this._onSelectionChange.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Queues a refresh, redrawing the selection on the next opportunity.
|
* 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.
|
* selection on Linux.
|
||||||
*/
|
*/
|
||||||
public refresh(isNewSelection?: boolean): void {
|
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||||
// Queue the refresh for the renderer
|
// Queue the refresh for the renderer
|
||||||
if (!this._refreshAnimationFrame) {
|
if (!this._refreshAnimationFrame) {
|
||||||
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
this._refreshAnimationFrame = window.requestAnimationFrame(() => this._refresh());
|
||||||
@@ -254,10 +262,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// If the platform is Linux and the refresh call comes from a mouse event,
|
// 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.
|
// we need to update the selection for middle click to paste selection.
|
||||||
if (Browser.isLinux && isNewSelection) {
|
if (Browser.isLinux && isLinuxMouseSelection) {
|
||||||
const selectionText = this.selectionText;
|
const selectionText = this.selectionText;
|
||||||
if (selectionText.length) {
|
if (selectionText.length) {
|
||||||
this.emit('newselection', this.selectionText);
|
this._onLinuxMouseSelection.fire(this.selectionText);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,7 +276,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
*/
|
*/
|
||||||
private _refresh(): void {
|
private _refresh(): void {
|
||||||
this._refreshAnimationFrame = null;
|
this._refreshAnimationFrame = null;
|
||||||
this.emit('refresh', {
|
this._onRedrawRequest.fire({
|
||||||
start: this._model.finalSelectionStart,
|
start: this._model.finalSelectionStart,
|
||||||
end: this._model.finalSelectionEnd,
|
end: this._model.finalSelectionEnd,
|
||||||
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
||||||
@@ -288,9 +296,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this._areCoordsInSelection(coords, start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _areCoordsInSelection(coords: [number, number], start: [number, number], end: [number, number]): boolean {
|
||||||
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
return (coords[1] > start[1] && coords[1] < end[1]) ||
|
||||||
(start[1] === end[1] && coords[1] === start[1] && coords[0] > start[0] && coords[0] < end[0]) ||
|
(start[1] === end[1] && coords[1] === start[1] && coords[0] >= start[0] && coords[0] < end[0]) ||
|
||||||
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]);
|
(start[1] < end[1] && coords[1] === end[1] && coords[0] < end[0]) ||
|
||||||
|
(start[1] < end[1] && coords[1] === start[1] && coords[0] >= start[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,7 +325,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
this._model.isSelectAllActive = true;
|
this._model.isSelectAllActive = true;
|
||||||
this.refresh();
|
this.refresh();
|
||||||
this._terminal.emit('selection');
|
this._onSelectionChange.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectLines(start: number, end: number): void {
|
public selectLines(start: number, end: number): void {
|
||||||
@@ -322,7 +335,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
this._model.selectionStart = [0, start];
|
this._model.selectionStart = [0, start];
|
||||||
this._model.selectionEnd = [this._terminal.cols, end];
|
this._model.selectionEnd = [this._terminal.cols, end];
|
||||||
this.refresh();
|
this.refresh();
|
||||||
this._terminal.emit('selection');
|
this._onSelectionChange.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -341,7 +354,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* @param event The mouse event.
|
* @param event The mouse event.
|
||||||
*/
|
*/
|
||||||
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
private _getMouseBufferCoords(event: MouseEvent): [number, number] {
|
||||||
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.options.lineHeight, this._terminal.cols, this._terminal.rows, true);
|
const coords = this._terminal.mouseHelper.getCoords(event, this._terminal.screenElement, this._charMeasure, this._terminal.cols, this._terminal.rows, true);
|
||||||
if (!coords) {
|
if (!coords) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -451,8 +464,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* Removes the listeners that are registered when mousedown is triggered.
|
* Removes the listeners that are registered when mousedown is triggered.
|
||||||
*/
|
*/
|
||||||
private _removeMouseDownListeners(): void {
|
private _removeMouseDownListeners(): void {
|
||||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
if (this._terminal.element.ownerDocument) {
|
||||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||||
|
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||||
|
}
|
||||||
clearInterval(this._dragScrollIntervalTimer);
|
clearInterval(this._dragScrollIntervalTimer);
|
||||||
this._dragScrollIntervalTimer = null;
|
this._dragScrollIntervalTimer = null;
|
||||||
}
|
}
|
||||||
@@ -498,8 +513,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// If the mouse is over the second half of a wide character, adjust the
|
// If the mouse is over the second half of a wide character, adjust the
|
||||||
// selection to cover the whole character
|
// selection to cover the whole character
|
||||||
const char = line[this._model.selectionStart[0]];
|
if (line.hasWidth(this._model.selectionStart[0]) === 0) {
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
|
||||||
this._model.selectionStart[0]++;
|
this._model.selectionStart[0]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -544,7 +558,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
*/
|
*/
|
||||||
private _onMouseMove(event: MouseEvent): void {
|
private _onMouseMove(event: MouseEvent): void {
|
||||||
// If the mousemove listener is active it means that a selection is
|
// 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.
|
// to be sent to the pty.
|
||||||
event.stopImmediatePropagation();
|
event.stopImmediatePropagation();
|
||||||
|
|
||||||
@@ -574,19 +588,21 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
||||||
|
|
||||||
// If the cursor was above or below the viewport, make sure it's at the
|
// If the cursor was above or below the viewport, make sure it's at the
|
||||||
// start or end of the viewport respectively.
|
// start or end of the viewport respectively. This should only happen when
|
||||||
if (this._dragScrollAmount > 0) {
|
// NOT in column select mode.
|
||||||
this._model.selectionEnd[0] = this._terminal.cols;
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
} else if (this._dragScrollAmount < 0) {
|
if (this._dragScrollAmount > 0) {
|
||||||
this._model.selectionEnd[0] = 0;
|
this._model.selectionEnd[0] = this._terminal.cols;
|
||||||
|
} else if (this._dragScrollAmount < 0) {
|
||||||
|
this._model.selectionEnd[0] = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the character is a wide character include the cell to the right in the
|
// If the character is a wide character include the cell to the right in the
|
||||||
// selection. Note that selections at the very end of the line will never
|
// selection. Note that selections at the very end of the line will never
|
||||||
// have a character.
|
// have a character.
|
||||||
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
||||||
const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
if (this._buffer.lines.get(this._model.selectionEnd[1]).hasWidth(this._model.selectionEnd[0]) === 0) {
|
||||||
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
|
||||||
this._model.selectionEnd[0]++;
|
this._model.selectionEnd[0]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -607,10 +623,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
if (this._dragScrollAmount) {
|
if (this._dragScrollAmount) {
|
||||||
this._terminal.scrollLines(this._dragScrollAmount, false);
|
this._terminal.scrollLines(this._dragScrollAmount, false);
|
||||||
// Re-evaluate selection
|
// Re-evaluate selection
|
||||||
|
// If the cursor was above or below the viewport, make sure it's at the
|
||||||
|
// start or end of the viewport respectively. This should only happen when
|
||||||
|
// NOT in column select mode.
|
||||||
if (this._dragScrollAmount > 0) {
|
if (this._dragScrollAmount > 0) {
|
||||||
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
|
this._model.selectionEnd[0] = this._terminal.cols;
|
||||||
|
}
|
||||||
|
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
|
||||||
} else {
|
} else {
|
||||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||||
|
this._model.selectionEnd[0] = 0;
|
||||||
|
}
|
||||||
|
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
|
||||||
}
|
}
|
||||||
this.refresh();
|
this.refresh();
|
||||||
}
|
}
|
||||||
@@ -628,7 +653,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME) {
|
||||||
(new AltClickHandler(event, this._terminal)).move();
|
(new AltClickHandler(event, this._terminal)).move();
|
||||||
} else if (this.hasSelection) {
|
} else if (this.hasSelection) {
|
||||||
this._terminal.emit('selection');
|
this._onSelectionChange.fire();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,8 +663,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
// reverseIndex) and delete in a splice is only ever used when the same
|
// reverseIndex) and delete in a splice is only ever used when the same
|
||||||
// number of elements was just added. Given this is could actually be
|
// number of elements was just added. Given this is could actually be
|
||||||
// beneficial to leave the selection as is for these cases.
|
// beneficial to leave the selection as is for these cases.
|
||||||
e.inactiveBuffer.lines.off('trim', this._trimListener);
|
if (this._trimListener) {
|
||||||
e.activeBuffer.lines.on('trim', this._trimListener);
|
this._trimListener.dispose();
|
||||||
|
}
|
||||||
|
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -647,19 +674,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* latter takes into account wide characters.
|
* latter takes into account wide characters.
|
||||||
* @param coords The coordinates to find the 2 index for.
|
* @param coords The coordinates to find the 2 index for.
|
||||||
*/
|
*/
|
||||||
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||||
let charIndex = coords[0];
|
let charIndex = coords[0];
|
||||||
for (let i = 0; coords[0] >= i; i++) {
|
for (let i = 0; coords[0] >= i; i++) {
|
||||||
const char = bufferLine[i];
|
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (this._workCell.getWidth() === 0) {
|
||||||
// Wide characters aren't included in the line string so decrement the
|
// Wide characters aren't included in the line string so decrement the
|
||||||
// index so the index is back on the wide character.
|
// index so the index is back on the wide character.
|
||||||
charIndex--;
|
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
|
// Emojis take up multiple characters, so adjust accordingly. For these
|
||||||
// we don't want ot include the character at the column as we're
|
// 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.
|
// returning the start index in the string, not the end index.
|
||||||
charIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
charIndex += length - 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return charIndex;
|
return charIndex;
|
||||||
@@ -677,7 +704,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* Gets positional information for the word at the coordinated specified.
|
* Gets positional information for the word at the coordinated specified.
|
||||||
* @param coords The coordinates to get the word at.
|
* @param coords The coordinates to get the word at.
|
||||||
*/
|
*/
|
||||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
|
||||||
// Ensure coords are within viewport (eg. not within scroll bar)
|
// Ensure coords are within viewport (eg. not within scroll bar)
|
||||||
if (coords[0] >= this._terminal.cols) {
|
if (coords[0] >= this._terminal.cols) {
|
||||||
return null;
|
return null;
|
||||||
@@ -719,48 +746,51 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Consider the initial position, skip it and increment the wide char
|
// Consider the initial position, skip it and increment the wide char
|
||||||
// variable
|
// variable
|
||||||
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (bufferLine.getWidth(startCol) === 0) {
|
||||||
leftWideCharCount++;
|
leftWideCharCount++;
|
||||||
startCol--;
|
startCol--;
|
||||||
}
|
}
|
||||||
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
|
if (bufferLine.getWidth(endCol) === 2) {
|
||||||
rightWideCharCount++;
|
rightWideCharCount++;
|
||||||
endCol++;
|
endCol++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||||
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
|
const length = bufferLine.getString(endCol).length;
|
||||||
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
if (length > 1) {
|
||||||
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
rightLongCharOffset += length - 1;
|
||||||
|
endIndex += length - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand the string in both directions until a space is hit
|
// Expand the string in both directions until a space is hit
|
||||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
|
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.loadCell(startCol - 1, this._workCell))) {
|
||||||
const char = bufferLine[startCol - 1];
|
bufferLine.loadCell(startCol - 1, this._workCell);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
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
|
// If the next character is a wide char, record it and skip the column
|
||||||
leftWideCharCount++;
|
leftWideCharCount++;
|
||||||
startCol--;
|
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),
|
// If the next character's string is longer than 1 char (eg. emoji),
|
||||||
// adjust the index
|
// adjust the index
|
||||||
leftLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
leftLongCharOffset += length - 1;
|
||||||
startIndex -= char[CHAR_DATA_CHAR_INDEX].length - 1;
|
startIndex -= length - 1;
|
||||||
}
|
}
|
||||||
startIndex--;
|
startIndex--;
|
||||||
startCol--;
|
startCol--;
|
||||||
}
|
}
|
||||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
|
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.loadCell(endCol + 1, this._workCell))) {
|
||||||
const char = bufferLine[endCol + 1];
|
bufferLine.loadCell(endCol + 1, this._workCell);
|
||||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
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
|
// If the next character is a wide char, record it and skip the column
|
||||||
rightWideCharCount++;
|
rightWideCharCount++;
|
||||||
endCol++;
|
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),
|
// If the next character's string is longer than 1 char (eg. emoji),
|
||||||
// adjust the index
|
// adjust the index
|
||||||
rightLongCharOffset += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
rightLongCharOffset += length - 1;
|
||||||
endIndex += char[CHAR_DATA_CHAR_INDEX].length - 1;
|
endIndex += length - 1;
|
||||||
}
|
}
|
||||||
endIndex++;
|
endIndex++;
|
||||||
endCol++;
|
endCol++;
|
||||||
@@ -772,7 +802,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Calculate the start _column_, converting the the string indexes back to
|
// Calculate the start _column_, converting the the string indexes back to
|
||||||
// column coordinates.
|
// column coordinates.
|
||||||
const start =
|
let start =
|
||||||
startIndex // The index of the selection's start char in the line string
|
startIndex // The index of the selection's start char in the line string
|
||||||
+ charOffset // The difference between the initial char's column and index
|
+ charOffset // The difference between the initial char's column and index
|
||||||
- leftWideCharCount // The number of wide chars left of the initial char
|
- leftWideCharCount // The number of wide chars left of the initial char
|
||||||
@@ -780,7 +810,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
|
|
||||||
// Calculate the length in _columns_, converting the the string indexes back
|
// Calculate the length in _columns_, converting the the string indexes back
|
||||||
// to column coordinates.
|
// to column coordinates.
|
||||||
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||||
endIndex // The index of the selection's end char in the line string
|
endIndex // The index of the selection's end char in the line string
|
||||||
- startIndex // The index of the selection's start char in the line string
|
- startIndex // The index of the selection's start char in the line string
|
||||||
+ leftWideCharCount // The number of wide chars left of the initial char
|
+ leftWideCharCount // The number of wide chars left of the initial char
|
||||||
@@ -792,6 +822,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||||
|
if (followWrappedLinesAbove) {
|
||||||
|
if (start === 0 && bufferLine.getCodePoint(0) !== 32 /*' '*/) {
|
||||||
|
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||||
|
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;
|
||||||
|
start -= offset;
|
||||||
|
length += offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||||
|
if (followWrappedLinesBelow) {
|
||||||
|
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.getCodePoint(0) !== 32 /*' '*/) {
|
||||||
|
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||||
|
if (nextLineWordPosition) {
|
||||||
|
length += nextLineWordPosition.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { start, length };
|
return { start, length };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -803,6 +861,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||||
if (wordPosition) {
|
if (wordPosition) {
|
||||||
|
// Adjust negative start value
|
||||||
|
while (wordPosition.start < 0) {
|
||||||
|
wordPosition.start += this._terminal.cols;
|
||||||
|
coords[1]--;
|
||||||
|
}
|
||||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||||
this._model.selectionStartLength = wordPosition.length;
|
this._model.selectionStartLength = wordPosition.length;
|
||||||
}
|
}
|
||||||
@@ -815,7 +878,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
private _selectToWordAt(coords: [number, number]): void {
|
private _selectToWordAt(coords: [number, number]): void {
|
||||||
const wordPosition = this._getWordAt(coords, true);
|
const wordPosition = this._getWordAt(coords, true);
|
||||||
if (wordPosition) {
|
if (wordPosition) {
|
||||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
let endRow = coords[1];
|
||||||
|
|
||||||
|
// Adjust negative start value
|
||||||
|
while (wordPosition.start < 0) {
|
||||||
|
wordPosition.start += this._terminal.cols;
|
||||||
|
endRow--;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
||||||
|
// case we're interested in the start of the word, not the end
|
||||||
|
if (!this._model.areSelectionValuesReversed()) {
|
||||||
|
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
|
||||||
|
wordPosition.length -= this._terminal.cols;
|
||||||
|
endRow++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,13 +904,13 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
|||||||
* word logic.
|
* word logic.
|
||||||
* @param char The character to check.
|
* @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
|
// Zero width characters are never separators as they are always to the
|
||||||
// right of wide characters
|
// right of wide characters
|
||||||
if (charData[CHAR_DATA_WIDTH_INDEX] === 0) {
|
if (cell.getWidth() === 0) {
|
||||||
return false;
|
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 { ITerminal } from './Types';
|
||||||
import { SelectionModel } from './SelectionModel';
|
import { SelectionModel } from './SelectionModel';
|
||||||
import { BufferSet } from './BufferSet';
|
import { BufferSet } from './BufferSet';
|
||||||
import { MockTerminal } from './utils/TestUtils.test';
|
import { MockTerminal } from './TestUtils.test';
|
||||||
|
|
||||||
class TestSelectionModel extends SelectionModel {
|
class TestSelectionModel extends SelectionModel {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -23,8 +23,8 @@ describe('SelectionManager', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
terminal = new MockTerminal();
|
terminal = new MockTerminal();
|
||||||
terminal.cols = 80;
|
(terminal as any).cols = 80;
|
||||||
terminal.rows = 2;
|
(terminal as any).rows = 2;
|
||||||
terminal.options.scrollback = 10;
|
terminal.options.scrollback = 10;
|
||||||
terminal.buffers = new BufferSet(terminal);
|
terminal.buffers = new BufferSet(terminal);
|
||||||
terminal.buffer = terminal.buffers.active;
|
terminal.buffer = terminal.buffers.active;
|
||||||
@@ -128,6 +128,11 @@ describe('SelectionManager', () => {
|
|||||||
model.selectionEnd = [3, 2];
|
model.selectionEnd = [3, 2];
|
||||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||||
});
|
});
|
||||||
|
it('should return the end on a different row when start + length overflows onto a following row', () => {
|
||||||
|
model.selectionStart = [78, 2];
|
||||||
|
model.selectionStartLength = 4;
|
||||||
|
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||||
|
});
|
||||||
it('should return selection end if selection end is after selection start + length', () => {
|
it('should return selection end if selection end is after selection start + length', () => {
|
||||||
model.selectionStart = [2, 2];
|
model.selectionStart = [2, 2];
|
||||||
model.selectionStartLength = 2;
|
model.selectionStartLength = 2;
|
||||||
|
|||||||
@@ -76,9 +76,13 @@ export class SelectionModel {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the selection start if the end doesn't exist or they're reversed
|
// Use the selection start + length if the end doesn't exist or they're reversed
|
||||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||||
|
if (startPlusLength > this._terminal.cols) {
|
||||||
|
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
|
||||||
|
}
|
||||||
|
return [startPlusLength, this.selectionStart[1]];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the the word/line is selected after a double/triple click
|
// Ensure the the word/line is selected after a double/triple click
|
||||||
|
|||||||
+22
-16
@@ -12,7 +12,19 @@ import { ITerminal, ISoundManager } from './Types';
|
|||||||
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
|
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
|
||||||
|
|
||||||
export class SoundManager implements ISoundManager {
|
export class SoundManager implements ISoundManager {
|
||||||
private _audioContext: AudioContext;
|
private static _audioContext: AudioContext;
|
||||||
|
|
||||||
|
static get audioContext(): AudioContext | null {
|
||||||
|
if (!SoundManager._audioContext) {
|
||||||
|
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
||||||
|
if (!audioContextCtor) {
|
||||||
|
console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SoundManager._audioContext = new audioContextCtor();
|
||||||
|
}
|
||||||
|
return SoundManager._audioContext;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _terminal: ITerminal
|
private _terminal: ITerminal
|
||||||
@@ -20,22 +32,16 @@ export class SoundManager implements ISoundManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public playBellSound(): void {
|
public playBellSound(): void {
|
||||||
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
const ctx = SoundManager.audioContext;
|
||||||
if (!this._audioContext && audioContextCtor) {
|
if (!ctx) {
|
||||||
this._audioContext = new audioContextCtor();
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (this._audioContext) {
|
|
||||||
const bellAudioSource = this._audioContext.createBufferSource();
|
|
||||||
const context = this._audioContext;
|
|
||||||
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
|
|
||||||
bellAudioSource.buffer = buffer;
|
|
||||||
bellAudioSource.connect(context.destination);
|
|
||||||
bellAudioSource.start(0);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
|
|
||||||
}
|
}
|
||||||
|
const bellAudioSource = ctx.createBufferSource();
|
||||||
|
ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
|
||||||
|
bellAudioSource.buffer = buffer;
|
||||||
|
bellAudioSource.connect(ctx.destination);
|
||||||
|
bellAudioSource.start(0);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
|
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import * as path from 'path';
|
|||||||
import * as pty from 'node-pty';
|
import * as pty from 'node-pty';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { Terminal } from './Terminal';
|
import { Terminal } from './Terminal';
|
||||||
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
|
|
||||||
import { IViewport } from './Types';
|
import { IViewport } from './Types';
|
||||||
|
import { CellData, WHITESPACE_CELL_CHAR } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
class TestTerminal extends Terminal {
|
class TestTerminal extends Terminal {
|
||||||
innerWrite(): void { this._innerWrite(); }
|
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++) {
|
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||||
lineText = '';
|
lineText = '';
|
||||||
for (let cell = 0; cell < term.cols; ++cell) {
|
for (let cell = 0; cell < term.cols; ++cell) {
|
||||||
lineText += term.buffer.lines.get(line)[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
|
// rtrim empty cells as xterm does
|
||||||
lineText = lineText.replace(/\s+$/, '');
|
lineText = lineText.replace(/\s+$/, '');
|
||||||
@@ -91,7 +91,8 @@ if (os.platform() !== 'win32') {
|
|||||||
primitivePty = (<any>pty).native.open(cols, rows);
|
primitivePty = (<any>pty).native.open(cols, rows);
|
||||||
|
|
||||||
/** tests */
|
/** tests */
|
||||||
describe('xterm output comparison', () => {
|
describe('xterm output comparison', function(): void {
|
||||||
|
this.timeout(10000);
|
||||||
let xterm: TestTerminal;
|
let xterm: TestTerminal;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -105,6 +106,7 @@ if (os.platform() !== 'win32') {
|
|||||||
// omit stack trace for escape sequence files
|
// omit stack trace for escape sequence files
|
||||||
Error.stackTraceLimit = 0;
|
Error.stackTraceLimit = 0;
|
||||||
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
||||||
|
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
|
||||||
// only successful tests for now
|
// only successful tests for now
|
||||||
const skip = [
|
const skip = [
|
||||||
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
||||||
@@ -112,8 +114,8 @@ if (os.platform() !== 'win32') {
|
|||||||
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||||
63, 68
|
63, 68
|
||||||
];
|
];
|
||||||
|
// These are failing on macOS only
|
||||||
if (os.platform() === 'darwin') {
|
if (os.platform() === 'darwin') {
|
||||||
// These are failing on macOS only
|
|
||||||
skip.push(3, 7, 11, 67);
|
skip.push(3, 7, 11, 67);
|
||||||
}
|
}
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
@@ -121,9 +123,9 @@ if (os.platform() !== 'win32') {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
((filename: string) => {
|
((filename: string) => {
|
||||||
|
const inFile = fs.readFileSync(filename, 'utf8');
|
||||||
it(filename.split('/').slice(-1)[0], done => {
|
it(filename.split('/').slice(-1)[0], done => {
|
||||||
ptyReset(() => {
|
ptyReset(() => {
|
||||||
const inFile = fs.readFileSync(filename, 'utf8');
|
|
||||||
ptyWriteRead(inFile, fromPty => {
|
ptyWriteRead(inFile, fromPty => {
|
||||||
// uncomment this to get log from terminal
|
// uncomment this to get log from terminal
|
||||||
// console.log = function(){};
|
// console.log = function(){};
|
||||||
@@ -163,7 +165,5 @@ describe('typings', () => {
|
|||||||
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
|
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
|
||||||
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
||||||
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
|
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
|
||||||
// Clean up
|
|
||||||
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+465
-227
@@ -5,8 +5,8 @@
|
|||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert, expect } from 'chai';
|
||||||
import { Terminal } from './Terminal';
|
import { Terminal } from './Terminal';
|
||||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
|
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
|
||||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
import { CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
|
||||||
|
|
||||||
const INIT_COLS = 80;
|
const INIT_COLS = 80;
|
||||||
const INIT_ROWS = 24;
|
const INIT_ROWS = 24;
|
||||||
@@ -16,7 +16,7 @@ class TestTerminal extends Terminal {
|
|||||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('term.js addons', () => {
|
describe('Terminal', () => {
|
||||||
let term: TestTerminal;
|
let term: TestTerminal;
|
||||||
const termOptions = {
|
const termOptions = {
|
||||||
cols: INIT_COLS,
|
cols: INIT_COLS,
|
||||||
@@ -25,7 +25,7 @@ describe('term.js addons', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term = new TestTerminal(termOptions);
|
term = new TestTerminal(termOptions);
|
||||||
term.refresh = () => {};
|
term.refresh = () => { };
|
||||||
(<any>term).renderer = new MockRenderer();
|
(<any>term).renderer = new MockRenderer();
|
||||||
term.viewport = new MockViewport();
|
term.viewport = new MockViewport();
|
||||||
(<any>term)._compositionHelper = new MockCompositionHelper();
|
(<any>term)._compositionHelper = new MockCompositionHelper();
|
||||||
@@ -36,8 +36,8 @@ describe('term.js addons', () => {
|
|||||||
};
|
};
|
||||||
(<any>term).element = {
|
(<any>term).element = {
|
||||||
classList: {
|
classList: {
|
||||||
toggle: () => {},
|
toggle: () => { },
|
||||||
remove: () => {}
|
remove: () => { }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -67,24 +67,251 @@ 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', () => { });
|
||||||
|
term.on('keypress', () => { });
|
||||||
|
term.on('keydown', () => { });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('data', () => {
|
||||||
|
it('should emit a data event', (done) => {
|
||||||
|
term.on('data', () => {
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.handler('fake');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if ((++steps) === 2) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const evKeyPress = <KeyboardEvent>{
|
||||||
|
preventDefault: () => { },
|
||||||
|
stopPropagation: () => { },
|
||||||
|
type: 'keypress',
|
||||||
|
keyCode: 13
|
||||||
|
};
|
||||||
|
|
||||||
|
term.on('keypress', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.on('key', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.keyPress(evKeyPress);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`keydown (including 'key' event)`, () => {
|
||||||
|
it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => {
|
||||||
|
let steps = 0;
|
||||||
|
|
||||||
|
const finish = () => {
|
||||||
|
if ((++steps) === 2) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const evKeyDown = <KeyboardEvent>{
|
||||||
|
preventDefault: () => { },
|
||||||
|
stopPropagation: () => { },
|
||||||
|
type: 'keydown',
|
||||||
|
keyCode: 13
|
||||||
|
};
|
||||||
|
|
||||||
|
term.on('keydown', (event) => {
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.on('key', (key, event) => {
|
||||||
|
assert.equal(typeof key, 'string');
|
||||||
|
expect(event).to.be.an.instanceof(Object);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.keyDown(evKeyDown);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resize', () => {
|
||||||
|
it('should receive an object: {cols: number, rows: number}', (done) => {
|
||||||
|
term.on('resize', (data) => {
|
||||||
|
expect(data).to.have.keys(['cols', 'rows']);
|
||||||
|
assert.equal(typeof data.cols, 'number');
|
||||||
|
assert.equal(typeof data.rows, 'number');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.resize(1, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('scroll', () => {
|
||||||
|
it('should receive a number', (done) => {
|
||||||
|
term.on('scroll', (ydisp) => {
|
||||||
|
assert.equal(typeof ydisp, 'number');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.scroll();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('title', () => {
|
||||||
|
it('should receive a string', (done) => {
|
||||||
|
term.on('title', (title) => {
|
||||||
|
assert.equal(typeof title, 'string');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
term.handleTitle('title');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('attachCustomKeyEventHandler', () => {
|
describe('attachCustomKeyEventHandler', () => {
|
||||||
const evKeyDown = <KeyboardEvent>{
|
const evKeyDown = <KeyboardEvent>{
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
keyCode: 77
|
keyCode: 77
|
||||||
};
|
};
|
||||||
const evKeyPress = <KeyboardEvent>{
|
const evKeyPress = <KeyboardEvent>{
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keypress',
|
type: 'keypress',
|
||||||
keyCode: 77
|
keyCode: 77
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term.handler = () => {};
|
term.handler = () => { };
|
||||||
term.showCursor = () => {};
|
term.showCursor = () => { };
|
||||||
term.clearSelection = () => {};
|
term.clearSelection = () => { };
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should process the keydown/keypress event based on what the handler returns', () => {
|
it('should process the keydown/keypress event based on what the handler returns', () => {
|
||||||
@@ -109,7 +336,7 @@ describe('term.js addons', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('setOption', () => {
|
describe('setOption', () => {
|
||||||
it('should set the option correctly', () => {
|
it('should set option correctly', () => {
|
||||||
term.setOption('cursorBlink', true);
|
term.setOption('cursorBlink', true);
|
||||||
assert.equal(term.options.cursorBlink, true);
|
assert.equal(term.options.cursorBlink, true);
|
||||||
term.setOption('cursorBlink', false);
|
term.setOption('cursorBlink', false);
|
||||||
@@ -141,7 +368,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should clear a buffer larger than rows', () => {
|
it('should clear a buffer larger than rows', () => {
|
||||||
@@ -158,7 +385,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('should not break the prompt when cleared twice', () => {
|
it('should not break the prompt when cleared twice', () => {
|
||||||
@@ -171,7 +398,7 @@ describe('term.js addons', () => {
|
|||||||
assert.equal(term.buffer.lines.length, term.rows);
|
assert.equal(term.buffer.lines.length, term.rows);
|
||||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||||
for (let i = 1; i < term.rows; i++) {
|
for (let i = 1; i < term.rows; i++) {
|
||||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -304,8 +531,8 @@ describe('term.js addons', () => {
|
|||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
key: 'a',
|
key: 'a',
|
||||||
keyCode: 65,
|
keyCode: 65,
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {}
|
stopPropagation: () => { }
|
||||||
};
|
};
|
||||||
|
|
||||||
term.buffer.ydisp = 0;
|
term.buffer.ydisp = 0;
|
||||||
@@ -337,62 +564,62 @@ describe('term.js addons', () => {
|
|||||||
describe('scroll() function', () => {
|
describe('scroll() function', () => {
|
||||||
describe('when scrollback > 0', () => {
|
describe('when scrollback > 0', () => {
|
||||||
it('should create a new line and scroll', () => {
|
it('should create a new line and scroll', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
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.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), 'b');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(INIT_ROWS).loadCell(0, new CellData()).getChars(), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
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.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||||
term.buffer.y = 3;
|
term.buffer.y = 3;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'b');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'c');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(3).loadCell(0, new CellData()).getChars(), 'd');
|
||||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
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)', () => {
|
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
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.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -403,65 +630,65 @@ describe('term.js addons', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should create a new line and shift everything up', () => {
|
it('should create a new line and shift everything up', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
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
|
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
// 'a' gets pushed out of buffer
|
// 'a' gets pushed out of buffer
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), '');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).loadCell(0, new CellData()).getChars(), 'c');
|
||||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).loadCell(0, new CellData()).getChars(), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
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.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
term.buffer.lines.get(4).setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)]));
|
||||||
term.buffer.y = 3;
|
term.buffer.y = 3;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'b');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
assert.equal(term.buffer.lines.get(1).loadCell(0, new CellData()).getChars(), 'c');
|
||||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
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)', () => {
|
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
term.buffer.lines.get(0).setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
term.buffer.lines.get(1).setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
term.buffer.lines.get(2).setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
term.buffer.lines.get(3).setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)]));
|
||||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
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.y = INIT_ROWS - 1; // Move cursor to last line
|
||||||
term.buffer.scrollTop = 1;
|
term.buffer.scrollTop = 1;
|
||||||
term.buffer.scrollBottom = 3;
|
term.buffer.scrollBottom = 3;
|
||||||
term.scroll();
|
term.scroll();
|
||||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
assert.equal(term.buffer.lines.get(0).loadCell(0, new CellData()).getChars(), 'a');
|
||||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
assert.equal(term.buffer.lines.get(2).loadCell(0, new CellData()).getChars(), 'd');
|
||||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
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)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
assert.equal(term.buffer.lines.get(4).loadCell(0, new CellData()).getChars(), 'e');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -472,9 +699,9 @@ describe('term.js addons', () => {
|
|||||||
let evKeyPress: any;
|
let evKeyPress: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
term.handler = () => {};
|
term.handler = () => { };
|
||||||
term.showCursor = () => {};
|
term.showCursor = () => { };
|
||||||
term.clearSelection = () => {};
|
term.clearSelection = () => { };
|
||||||
// term.compositionHelper = {
|
// term.compositionHelper = {
|
||||||
// isComposing: false,
|
// isComposing: false,
|
||||||
// keydown: {
|
// keydown: {
|
||||||
@@ -484,15 +711,15 @@ describe('term.js addons', () => {
|
|||||||
// }
|
// }
|
||||||
// };
|
// };
|
||||||
evKeyDown = {
|
evKeyDown = {
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keydown',
|
type: 'keydown',
|
||||||
altKey: null,
|
altKey: null,
|
||||||
keyCode: null
|
keyCode: null
|
||||||
};
|
};
|
||||||
evKeyPress = {
|
evKeyPress = {
|
||||||
preventDefault: () => {},
|
preventDefault: () => { },
|
||||||
stopPropagation: () => {},
|
stopPropagation: () => { },
|
||||||
type: 'keypress',
|
type: 'keypress',
|
||||||
altKey: null,
|
altKey: null,
|
||||||
charCode: null,
|
charCode: null,
|
||||||
@@ -652,116 +879,126 @@ describe('term.js addons', () => {
|
|||||||
it('2 characters per cell', function (): void {
|
it('2 characters per cell', function (): void {
|
||||||
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
|
const cell = new CellData();
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.write(high + String.fromCharCode(i));
|
term.write(high + String.fromCharCode(i));
|
||||||
const tchar = term.buffer.lines.get(0)[0];
|
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar.getWidth()).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('2 characters at last cell', () => {
|
it('2 characters at last cell', () => {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
|
const cell = new CellData();
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.buffer.x = term.cols - 1;
|
term.buffer.x = term.cols - 1;
|
||||||
term.write(high + String.fromCharCode(i));
|
term.write(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(0).loadCell(term.buffer.x - 1, cell).getChars().length).eql(2);
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql('');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('2 characters per cell over line end with autowrap', () => {
|
it('2 characters per cell over line end with autowrap', () => {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
|
const cell = new CellData();
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.buffer.x = term.cols - 1;
|
term.buffer.x = term.cols - 1;
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write('a' + high + String.fromCharCode(i));
|
term.write('a' + high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars()).eql(high + String.fromCharCode(i));
|
||||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(term.buffer.lines.get(1).loadCell(0, cell).getChars().length).eql(2);
|
||||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('2 characters per cell over line end without autowrap', () => {
|
it('2 characters per cell over line end without autowrap', () => {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
|
const cell = new CellData();
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.buffer.x = term.cols - 1;
|
term.buffer.x = term.cols - 1;
|
||||||
term.wraparoundMode = false;
|
term.wraparoundMode = false;
|
||||||
term.write('a' + high + String.fromCharCode(i));
|
term.write('a' + high + String.fromCharCode(i));
|
||||||
// auto wraparound mode should cut off the rest of the line
|
// auto wraparound mode should cut off the rest of the line
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars()).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(term.buffer.lines.get(0).loadCell(term.cols - 1, cell).getChars().length).eql(1);
|
||||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(1).loadCell(1, cell).getChars()).eql('');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
it('splitted surrogates', () => {
|
it('splitted surrogates', () => {
|
||||||
const high = String.fromCharCode(0xD800);
|
const high = String.fromCharCode(0xD800);
|
||||||
|
const cell = new CellData();
|
||||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||||
term.write(high);
|
term.write(high);
|
||||||
term.write(String.fromCharCode(i));
|
term.write(String.fromCharCode(i));
|
||||||
const tchar = term.buffer.lines.get(0)[0];
|
const tchar = term.buffer.lines.get(0).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
expect(tchar.getChars()).eql(high + String.fromCharCode(i));
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(tchar.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(tchar.getWidth()).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(term.buffer.lines.get(0).loadCell(1, cell).getChars()).eql('');
|
||||||
term.reset();
|
term.reset();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('unicode - combining characters', () => {
|
describe('unicode - combining characters', () => {
|
||||||
|
const cell = new CellData();
|
||||||
it('café', () => {
|
it('café', () => {
|
||||||
term.write('cafe\u0301');
|
term.write('cafe\u0301');
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
term.buffer.lines.get(0).loadCell(3, cell);
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars()).eql('e\u0301');
|
||||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getChars().length).eql(2);
|
||||||
|
expect(cell.getWidth()).eql(1);
|
||||||
});
|
});
|
||||||
it('café - end of line', () => {
|
it('café - end of line', () => {
|
||||||
term.buffer.x = term.cols - 1 - 3;
|
term.buffer.x = term.cols - 1 - 3;
|
||||||
term.write('cafe\u0301');
|
term.write('cafe\u0301');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars()).eql('e\u0301');
|
||||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(cell.getWidth()).eql(1);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
term.buffer.lines.get(0).loadCell(1, cell);
|
||||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getChars()).eql('');
|
||||||
|
expect(cell.getChars().length).eql(0);
|
||||||
|
expect(cell.getWidth()).eql(1);
|
||||||
});
|
});
|
||||||
it('multiple combined é', () => {
|
it('multiple combined é', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(100).join('e\u0301'));
|
term.write(Array(100).join('e\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(cell.getChars()).eql('e\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
expect(cell.getChars()).eql('e\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
});
|
});
|
||||||
it('multiple surrogate with combined', () => {
|
it('multiple surrogate with combined', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
expect(cell.getChars()).eql('\uD800\uDC00\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('unicode - fullwidth characters', () => {
|
describe('unicode - fullwidth characters', () => {
|
||||||
|
const cell = new CellData();
|
||||||
it('cursor movement even', () => {
|
it('cursor movement even', () => {
|
||||||
expect(term.buffer.x).eql(0);
|
expect(term.buffer.x).eql(0);
|
||||||
term.write('¥');
|
term.write('¥');
|
||||||
@@ -777,140 +1014,141 @@ describe('term.js addons', () => {
|
|||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('¥'));
|
term.write(Array(50).join('¥'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(cell.getChars()).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(cell.getChars()).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
it('line of ¥ odd', () => {
|
it('line of ¥ odd', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('¥'));
|
term.write(Array(50).join('¥'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(cell.getChars()).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(cell.getChars()).eql('¥');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(1);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
it('line of ¥ with combining odd', () => {
|
it('line of ¥ with combining odd', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('¥\u0301'));
|
term.write(Array(50).join('¥\u0301'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(cell.getChars()).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(cell.getChars()).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
it('line of ¥ with combining even', () => {
|
it('line of ¥ with combining even', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('¥\u0301'));
|
term.write(Array(50).join('¥\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(cell.getChars()).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
expect(cell.getChars()).eql('¥\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
expect(cell.getChars().length).eql(2);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
it('line of surrogate fullwidth with combining odd', () => {
|
it('line of surrogate fullwidth with combining odd', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.buffer.x = 1;
|
term.buffer.x = 1;
|
||||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (let i = 1; i < term.cols - 1; ++i) {
|
for (let i = 1; i < term.cols - 1; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (!(i % 2)) {
|
if (!(i % 2)) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
term.buffer.lines.get(0).loadCell(term.cols - 1, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
expect(cell.getWidth()).eql(1);
|
||||||
tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
it('line of surrogate fullwidth with combining even', () => {
|
it('line of surrogate fullwidth with combining even', () => {
|
||||||
term.wraparoundMode = true;
|
term.wraparoundMode = true;
|
||||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||||
for (let i = 0; i < term.cols; ++i) {
|
for (let i = 0; i < term.cols; ++i) {
|
||||||
const tchar = term.buffer.lines.get(0)[i];
|
term.buffer.lines.get(0).loadCell(i, cell);
|
||||||
if (i % 2) {
|
if (i % 2) {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(cell.getChars()).eql('');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
expect(cell.getChars().length).eql(0);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(0);
|
expect(cell.getWidth()).eql(0);
|
||||||
} else {
|
} else {
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const tchar = term.buffer.lines.get(1)[0];
|
term.buffer.lines.get(1).loadCell(0, cell);
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
expect(cell.getChars()).eql('\ud843\ude6d\u0301');
|
||||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
expect(cell.getChars().length).eql(3);
|
||||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
expect(cell.getWidth()).eql(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('insert mode', () => {
|
describe('insert mode', () => {
|
||||||
|
const cell = new CellData();
|
||||||
it('halfwidth - all', () => {
|
it('halfwidth - all', () => {
|
||||||
term.write(Array(9).join('0123456789').slice(-80));
|
term.write(Array(9).join('0123456789').slice(-80));
|
||||||
term.buffer.x = 10;
|
term.buffer.x = 10;
|
||||||
@@ -918,10 +1156,10 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('abcde');
|
term.write('abcde');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e');
|
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('e');
|
||||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0');
|
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('0');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
|
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('4');
|
||||||
});
|
});
|
||||||
it('fullwidth - insert', () => {
|
it('fullwidth - insert', () => {
|
||||||
term.write(Array(9).join('0123456789').slice(-80));
|
term.write(Array(9).join('0123456789').slice(-80));
|
||||||
@@ -930,11 +1168,11 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('¥¥¥');
|
term.write('¥¥¥');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('');
|
||||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).loadCell(14, cell).getChars()).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('');
|
expect(term.buffer.lines.get(0).loadCell(15, cell).getChars()).eql('');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3');
|
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql('3');
|
||||||
});
|
});
|
||||||
it('fullwidth - right border', () => {
|
it('fullwidth - right border', () => {
|
||||||
term.write(Array(41).join('¥'));
|
term.write(Array(41).join('¥'));
|
||||||
@@ -943,14 +1181,14 @@ describe('term.js addons', () => {
|
|||||||
term.insertMode = true;
|
term.insertMode = true;
|
||||||
term.write('a');
|
term.write('a');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
expect(term.buffer.lines.get(0).loadCell(10, cell).getChars()).eql('a');
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // fullwidth char got replaced
|
||||||
term.write('b');
|
term.write('b');
|
||||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b');
|
expect(term.buffer.lines.get(0).loadCell(11, cell).getChars()).eql('b');
|
||||||
expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
expect(term.buffer.lines.get(0).loadCell(12, cell).getChars()).eql('¥');
|
||||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
expect(term.buffer.lines.get(0).loadCell(79, cell).getChars()).eql(''); // empty cell after fullwidth
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+442
-334
File diff suppressed because it is too large
Load Diff
@@ -3,13 +3,34 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from '../renderer/Types';
|
import { IRenderer, IRenderDimensions } from './renderer/Types';
|
||||||
import { LineData, IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ICircularList, ILinkifier, IMouseHelper, ILinkMatcherOptions, XtermListener } from '../Types';
|
import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
|
||||||
import { Buffer } from '../Buffer';
|
import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||||
import * as Browser from '../shared/utils/Browser';
|
import { ICircularList, XtermListener } from './common/Types';
|
||||||
import { ITheme, IDisposable, IMarker } from 'xterm';
|
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 {
|
||||||
|
this.writeBuffer.push(data);
|
||||||
|
this._innerWrite();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class MockTerminal implements ITerminal {
|
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[];
|
markers: IMarker[];
|
||||||
addMarker(cursorYOffset: number): IMarker {
|
addMarker(cursorYOffset: number): IMarker {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
@@ -45,6 +66,12 @@ export class MockTerminal implements ITerminal {
|
|||||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||||
throw new Error('Method not implemented.');
|
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 {
|
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -57,9 +84,15 @@ export class MockTerminal implements ITerminal {
|
|||||||
getSelection(): string {
|
getSelection(): string {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
getSelectionPosition(): ISelectionPosition | undefined {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
clearSelection(): void {
|
clearSelection(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
select(column: number, row: number, length: number): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
selectAll(): void {
|
selectAll(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -84,7 +117,7 @@ export class MockTerminal implements ITerminal {
|
|||||||
write(data: string): void {
|
write(data: string): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
send(data: string): void {
|
writeUtf8(data: Uint8Array): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
@@ -115,7 +148,7 @@ export class MockTerminal implements ITerminal {
|
|||||||
handler(data: string): void {
|
handler(data: string): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
on(event: string, callback: () => void): void {
|
on(event: string, callback: (...args: any[]) => void): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
off(type: string, listener: XtermListener): void {
|
off(type: string, listener: XtermListener): void {
|
||||||
@@ -148,17 +181,12 @@ export class MockTerminal implements ITerminal {
|
|||||||
refresh(start: number, end: number): void {
|
refresh(start: number, end: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData {
|
registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||||
const line: LineData = [];
|
deregisterCharacterJoiner(joinerId: number): void { }
|
||||||
cols = cols || this.cols;
|
|
||||||
for (let i = 0; i < cols; i++) {
|
|
||||||
line.push([0, ' ', 1, 32]);
|
|
||||||
}
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockCharMeasure implements ICharMeasure {
|
export class MockCharMeasure implements ICharMeasure {
|
||||||
|
onCharSizeChanged: IEvent<void>;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
measure(options: ITerminalOptions): void {
|
measure(options: ITerminalOptions): void {
|
||||||
@@ -181,8 +209,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
insertMode: boolean;
|
insertMode: boolean;
|
||||||
wraparoundMode: boolean;
|
wraparoundMode: boolean;
|
||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
curAttr: number;
|
curAttrData = new AttributeData();
|
||||||
savedCurAttr: number;
|
|
||||||
savedCols: number;
|
savedCols: number;
|
||||||
x10Mouse: boolean;
|
x10Mouse: boolean;
|
||||||
vt200Mouse: boolean;
|
vt200Mouse: boolean;
|
||||||
@@ -217,7 +244,7 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
setgLevel(g: number): void {
|
setgLevel(g: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
eraseAttr(): number {
|
eraseAttrData(): IAttributeData {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
eraseRight(x: number, y: number): void {
|
eraseRight(x: number, y: number): void {
|
||||||
@@ -229,18 +256,12 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
eraseLeft(x: number, y: number): void {
|
eraseLeft(x: number, y: number): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean): [number, string, number, number][] {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
prevStop(x?: number): number {
|
prevStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
is(term: string): boolean {
|
is(term: string): boolean {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
send(data: string): void {
|
|
||||||
throw new Error('Method not implemented.');
|
|
||||||
}
|
|
||||||
setgCharset(g: number, charset: { [key: string]: string; }): void {
|
setgCharset(g: number, charset: { [key: string]: string; }): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -283,11 +304,23 @@ export class MockInputHandlingTerminal implements IInputHandlingTerminal {
|
|||||||
tabSet(): void {
|
tabSet(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
handler(data: string): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
handleTitle(title: string): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
index(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
reverseIndex(): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockBuffer implements IBuffer {
|
export class MockBuffer implements IBuffer {
|
||||||
isCursorInViewport: boolean;
|
isCursorInViewport: boolean;
|
||||||
lines: ICircularList<[number, string, number, number][]>;
|
lines: ICircularList<IBufferLine>;
|
||||||
ydisp: number;
|
ydisp: number;
|
||||||
ybase: number;
|
ybase: number;
|
||||||
hasScrollback: boolean;
|
hasScrollback: boolean;
|
||||||
@@ -298,11 +331,12 @@ export class MockBuffer implements IBuffer {
|
|||||||
scrollTop: number;
|
scrollTop: number;
|
||||||
savedY: number;
|
savedY: number;
|
||||||
savedX: number;
|
savedX: number;
|
||||||
|
savedCurAttrData = new AttributeData();
|
||||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
|
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string {
|
||||||
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
return Buffer.prototype.translateBufferLineToString.apply(this, arguments);
|
||||||
}
|
}
|
||||||
getWrappedRangeForLine(y: number): { first: number; last: number; } {
|
getWrappedRangeForLine(y: number): { first: number; last: number; } {
|
||||||
throw new Error('Method not implemented.');
|
return Buffer.prototype.getWrappedRangeForLine.apply(this, arguments);
|
||||||
}
|
}
|
||||||
nextStop(x?: number): number {
|
nextStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
@@ -310,12 +344,29 @@ export class MockBuffer implements IBuffer {
|
|||||||
prevStop(x?: number): number {
|
prevStop(x?: number): number {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
setLines(lines: ICircularList<[number, string, number, number][]>): void {
|
setLines(lines: ICircularList<IBufferLine>): void {
|
||||||
this.lines = lines;
|
this.lines = lines;
|
||||||
}
|
}
|
||||||
|
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
|
||||||
|
return Buffer.prototype.getBlankLine.apply(this, arguments);
|
||||||
|
}
|
||||||
|
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[] {
|
||||||
|
return Buffer.prototype.stringIndexToBufferIndex.apply(this, arguments);
|
||||||
|
}
|
||||||
|
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 {
|
export class MockRenderer implements IRenderer {
|
||||||
|
onCanvasResize: IEvent<{ width: number; height: number; }>;
|
||||||
|
onRender: IEvent<{ start: number; end: number; }>;
|
||||||
dispose(): void {
|
dispose(): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@@ -333,7 +384,9 @@ export class MockRenderer implements IRenderer {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
dimensions: IRenderDimensions;
|
dimensions: IRenderDimensions;
|
||||||
setTheme(theme: ITheme): IColorSet { return <IColorSet>{}; }
|
setColors(colors: IColorSet): void {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
onResize(cols: number, rows: number): void {}
|
onResize(cols: number, rows: number): void {}
|
||||||
onCharSizeChanged(): void {}
|
onCharSizeChanged(): void {}
|
||||||
onBlur(): void {}
|
onBlur(): void {}
|
||||||
@@ -341,9 +394,11 @@ export class MockRenderer implements IRenderer {
|
|||||||
onSelectionChanged(start: [number, number], end: [number, number]): void {}
|
onSelectionChanged(start: [number, number], end: [number, number]): void {}
|
||||||
onCursorMove(): void {}
|
onCursorMove(): void {}
|
||||||
onOptionsChanged(): void {}
|
onOptionsChanged(): void {}
|
||||||
onWindowResize(devicePixelRatio: number): void {}
|
onDevicePixelRatioChange(): void {}
|
||||||
clear(): 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; }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MockViewport implements IViewport {
|
export class MockViewport implements IViewport {
|
||||||
@@ -351,7 +406,7 @@ export class MockViewport implements IViewport {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
scrollBarWidth: number = 0;
|
scrollBarWidth: number = 0;
|
||||||
onThemeChanged(colors: IColorSet): void {
|
onThemeChange(colors: IColorSet): void {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
onWheel(ev: WheelEvent): void {
|
onWheel(ev: WheelEvent): void {
|
||||||
+145
-59
@@ -3,26 +3,23 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
|
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||||
import { IColorSet, IRenderer } from './renderer/Types';
|
import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
|
||||||
import { IMouseZoneManager } from './ui/Types';
|
import { ICircularList } from './common/Types';
|
||||||
import { ICharset } from './core/Types';
|
import { IEvent } from './common/EventEmitter2';
|
||||||
|
import { IColorSet } from './ui/Types';
|
||||||
|
|
||||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||||
|
|
||||||
export type XtermListener = (...args: any[]) => void;
|
|
||||||
|
|
||||||
export type CharData = [number, string, number, number];
|
|
||||||
export type LineData = CharData[];
|
export type LineData = CharData[];
|
||||||
|
|
||||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||||
|
|
||||||
export const enum LinkHoverEventTypes {
|
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||||
HOVER = 'linkhover',
|
|
||||||
TOOLTIP = 'linktooltip',
|
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
|
||||||
LEAVE = 'linkleave'
|
export type BufferIndex = [number, number];
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This interface encapsulates everything needed from the Terminal by the
|
* This interface encapsulates everything needed from the Terminal by the
|
||||||
@@ -44,7 +41,7 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
insertMode: boolean;
|
insertMode: boolean;
|
||||||
wraparoundMode: boolean;
|
wraparoundMode: boolean;
|
||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
curAttr: number;
|
curAttrData: IAttributeData;
|
||||||
savedCols: number;
|
savedCols: number;
|
||||||
x10Mouse: boolean;
|
x10Mouse: boolean;
|
||||||
vt200Mouse: boolean;
|
vt200Mouse: boolean;
|
||||||
@@ -63,27 +60,24 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
|||||||
|
|
||||||
bell(): void;
|
bell(): void;
|
||||||
focus(): void;
|
focus(): void;
|
||||||
convertEol: boolean;
|
|
||||||
updateRange(y: number): void;
|
updateRange(y: number): void;
|
||||||
scroll(isWrapped?: boolean): void;
|
scroll(isWrapped?: boolean): void;
|
||||||
setgLevel(g: number): void;
|
setgLevel(g: number): void;
|
||||||
eraseAttr(): number;
|
eraseAttrData(): IAttributeData;
|
||||||
eraseRight(x: number, y: number): void;
|
|
||||||
eraseLine(y: number): void;
|
|
||||||
eraseLeft(x: number, y: number): void;
|
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
|
||||||
is(term: string): boolean;
|
is(term: string): boolean;
|
||||||
send(data: string): void;
|
|
||||||
setgCharset(g: number, charset: ICharset): void;
|
setgCharset(g: number, charset: ICharset): void;
|
||||||
resize(x: number, y: number): void;
|
resize(x: number, y: number): void;
|
||||||
log(text: string, data?: any): void;
|
log(text: string, data?: any): void;
|
||||||
reset(): void;
|
reset(): void;
|
||||||
showCursor(): void;
|
showCursor(): void;
|
||||||
refresh(start: number, end: number): void;
|
refresh(start: number, end: number): void;
|
||||||
matchColor(r1: number, g1: number, b1: number): number;
|
|
||||||
error(text: string, data?: any): void;
|
error(text: string, data?: any): void;
|
||||||
setOption(key: string, value: any): void;
|
setOption(key: string, value: any): void;
|
||||||
tabSet(): void;
|
tabSet(): void;
|
||||||
|
handler(data: string): void;
|
||||||
|
handleTitle(title: string): void;
|
||||||
|
index(): void;
|
||||||
|
reverseIndex(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IViewport extends IDisposable {
|
export interface IViewport extends IDisposable {
|
||||||
@@ -93,7 +87,7 @@ export interface IViewport extends IDisposable {
|
|||||||
onWheel(ev: WheelEvent): void;
|
onWheel(ev: WheelEvent): void;
|
||||||
onTouchStart(ev: TouchEvent): void;
|
onTouchStart(ev: TouchEvent): void;
|
||||||
onTouchMove(ev: TouchEvent): void;
|
onTouchMove(ev: TouchEvent): void;
|
||||||
onThemeChanged(colors: IColorSet): void;
|
onThemeChange(colors: IColorSet): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICompositionHelper {
|
export interface ICompositionHelper {
|
||||||
@@ -109,7 +103,8 @@ export interface ICompositionHelper {
|
|||||||
*/
|
*/
|
||||||
export interface IInputHandler {
|
export interface IInputHandler {
|
||||||
parse(data: string): void;
|
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 BEL */ bell(): void;
|
||||||
/** C0 LF */ lineFeed(): void;
|
/** C0 LF */ lineFeed(): void;
|
||||||
@@ -192,19 +187,19 @@ export interface ILinkMatcher {
|
|||||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkHoverEvent {
|
export interface ILinkifierEvent {
|
||||||
x1: number;
|
x1: number;
|
||||||
y1: number;
|
y1: number;
|
||||||
x2: number;
|
x2: number;
|
||||||
y2: number;
|
y2: number;
|
||||||
cols: number;
|
cols: number;
|
||||||
|
fg: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||||
screenElement: HTMLElement;
|
screenElement: HTMLElement;
|
||||||
selectionManager: ISelectionManager;
|
selectionManager: ISelectionManager;
|
||||||
charMeasure: ICharMeasure;
|
charMeasure: ICharMeasure;
|
||||||
renderer: IRenderer;
|
|
||||||
browser: IBrowser;
|
browser: IBrowser;
|
||||||
writeBuffer: string[];
|
writeBuffer: string[];
|
||||||
cursorHidden: boolean;
|
cursorHidden: boolean;
|
||||||
@@ -218,17 +213,63 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
|||||||
bracketedPasteMode: boolean;
|
bracketedPasteMode: boolean;
|
||||||
applicationCursor: 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;
|
handler(data: string): void;
|
||||||
send(data: string): void;
|
|
||||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||||
cancel(ev: Event, force?: boolean): boolean | void;
|
cancel(ev: Event, force?: boolean): boolean | void;
|
||||||
log(text: string): void;
|
log(text: string): void;
|
||||||
showCursor(): void;
|
showCursor(): void;
|
||||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
}
|
||||||
|
|
||||||
|
// 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 {
|
export interface IBufferAccessor {
|
||||||
@@ -236,7 +277,7 @@ export interface IBufferAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IElementAccessor {
|
export interface IElementAccessor {
|
||||||
element: HTMLElement;
|
readonly element: HTMLElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkifierAccessor {
|
export interface ILinkifierAccessor {
|
||||||
@@ -244,13 +285,16 @@ export interface ILinkifierAccessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IMouseHelper {
|
export interface IMouseHelper {
|
||||||
getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
getCoords(event: { clientX: number, clientY: number }, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
|
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, colCount: number, rowCount: number): { x: number, y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICharMeasure {
|
export interface ICharMeasure {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
|
||||||
|
onCharSizeChanged: IEvent<void>;
|
||||||
|
|
||||||
measure(options: ITerminalOptions): void;
|
measure(options: ITerminalOptions): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,8 +310,18 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
|||||||
useFlowControl?: boolean;
|
useFlowControl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IBufferStringIteratorResult {
|
||||||
|
range: {first: number, last: number};
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IBufferStringIterator {
|
||||||
|
hasNext(): boolean;
|
||||||
|
next(): IBufferStringIteratorResult;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IBuffer {
|
export interface IBuffer {
|
||||||
readonly lines: ICircularList<LineData>;
|
readonly lines: ICircularList<IBufferLine>;
|
||||||
ydisp: number;
|
ydisp: number;
|
||||||
ybase: number;
|
ybase: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -278,33 +332,28 @@ export interface IBuffer {
|
|||||||
hasScrollback: boolean;
|
hasScrollback: boolean;
|
||||||
savedY: number;
|
savedY: number;
|
||||||
savedX: number;
|
savedX: number;
|
||||||
|
savedCurAttrData: IAttributeData;
|
||||||
isCursorInViewport: boolean;
|
isCursorInViewport: boolean;
|
||||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||||
getWrappedRangeForLine(y: number): { first: number, last: number };
|
getWrappedRangeForLine(y: number): { first: number, last: number };
|
||||||
nextStop(x?: number): number;
|
nextStop(x?: number): number;
|
||||||
prevStop(x?: number): number;
|
prevStop(x?: number): number;
|
||||||
|
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;
|
alt: IBuffer;
|
||||||
normal: IBuffer;
|
normal: IBuffer;
|
||||||
active: IBuffer;
|
active: IBuffer;
|
||||||
|
|
||||||
|
onBufferActivate: IEvent<{ activeBuffer: IBuffer, inactiveBuffer: IBuffer }>;
|
||||||
|
|
||||||
activateNormalBuffer(): void;
|
activateNormalBuffer(): void;
|
||||||
activateAltBuffer(): void;
|
activateAltBuffer(fillAttr?: IAttributeData): void;
|
||||||
}
|
|
||||||
|
|
||||||
export interface ICircularList<T> extends IEventEmitter {
|
|
||||||
length: number;
|
|
||||||
maxLength: number;
|
|
||||||
|
|
||||||
get(index: number): T;
|
|
||||||
set(index: number, value: T): void;
|
|
||||||
push(value: T): void;
|
|
||||||
pop(): T;
|
|
||||||
splice(start: number, deleteCount: number, ...items: T[]): void;
|
|
||||||
trimStart(count: number): void;
|
|
||||||
shiftElements(start: number, count: number, offset: number): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISelectionManager {
|
export interface ISelectionManager {
|
||||||
@@ -319,7 +368,17 @@ export interface ISelectionManager {
|
|||||||
selectWordAtCursor(event: MouseEvent): void;
|
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;
|
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||||
linkifyRows(start: number, end: number): void;
|
linkifyRows(start: number, end: number): void;
|
||||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||||
@@ -450,18 +509,26 @@ export interface IParsingState {
|
|||||||
* DCS handler signature for EscapeSequenceParser.
|
* DCS handler signature for EscapeSequenceParser.
|
||||||
* EscapeSequenceParser handles DCS commands via separate
|
* EscapeSequenceParser handles DCS commands via separate
|
||||||
* subparsers that get hook/unhooked and can handle
|
* 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
|
* On entering a DSC sequence `hook` is called by
|
||||||
* `EscapeSequenceParser`. Use it to initialize or reset
|
* `EscapeSequenceParser`. Use it to initialize or reset
|
||||||
* states needed to handle the current DCS sequence.
|
* 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
|
* EscapeSequenceParser will call `put` several times if the
|
||||||
* parsed string got splitted, therefore you might have to collect
|
* parsed data got split, therefore you might have to collect
|
||||||
* `data` until `unhook` is called. `unhook` marks the end
|
* `data` until `unhook` is called.
|
||||||
* of the current DCS sequence.
|
* 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 {
|
export interface IDcsHandler {
|
||||||
hook(collect: string, params: number[], flag: number): void;
|
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;
|
unhook(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,9 +545,9 @@ export interface IEscapeSequenceParser extends IDisposable {
|
|||||||
* Parse string `data`.
|
* Parse string `data`.
|
||||||
* @param data The data to parse.
|
* @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;
|
clearPrintHandler(): void;
|
||||||
|
|
||||||
setExecuteHandler(flag: string, callback: () => void): void;
|
setExecuteHandler(flag: string, callback: () => void): void;
|
||||||
@@ -490,6 +557,8 @@ export interface IEscapeSequenceParser extends IDisposable {
|
|||||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
|
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
|
||||||
clearCsiHandler(flag: string): void;
|
clearCsiHandler(flag: string): void;
|
||||||
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): 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;
|
setEscHandler(collectAndFlag: string, callback: () => void): void;
|
||||||
clearEscHandler(collectAndFlag: string): void;
|
clearEscHandler(collectAndFlag: string): void;
|
||||||
@@ -506,3 +575,20 @@ export interface IEscapeSequenceParser extends IDisposable {
|
|||||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
||||||
clearErrorHandler(): void;
|
clearErrorHandler(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IMouseZoneManager extends IDisposable {
|
||||||
|
add(zone: IMouseZone): void;
|
||||||
|
clearAll(start?: number, end?: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|||||||
+76
-28
@@ -3,11 +3,12 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IColorSet } from './renderer/Types';
|
|
||||||
import { ITerminal, IViewport } from './Types';
|
import { ITerminal, IViewport } from './Types';
|
||||||
import { CharMeasure } from './ui/CharMeasure';
|
import { CharMeasure } from './CharMeasure';
|
||||||
import { Disposable } from './common/Lifecycle';
|
import { Disposable } from './common/Lifecycle';
|
||||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||||
|
import { IColorSet } from './ui/Types';
|
||||||
|
import { IRenderDimensions } from './renderer/Types';
|
||||||
|
|
||||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||||
|
|
||||||
@@ -22,12 +23,16 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
private _lastRecordedViewportHeight: number = 0;
|
private _lastRecordedViewportHeight: number = 0;
|
||||||
private _lastRecordedBufferHeight: number = 0;
|
private _lastRecordedBufferHeight: number = 0;
|
||||||
private _lastTouchY: number;
|
private _lastTouchY: number;
|
||||||
|
private _lastScrollTop: number = 0;
|
||||||
|
|
||||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||||
private _wheelPartialScroll: number = 0;
|
private _wheelPartialScroll: number = 0;
|
||||||
|
|
||||||
|
private _refreshAnimationFrame: number | null = null;
|
||||||
|
private _ignoreNextScrollEvent: boolean = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Viewport.
|
* Creates a new Viewport.
|
||||||
* @param _terminal The terminal this viewport belongs to.
|
* @param _terminal The terminal this viewport belongs to.
|
||||||
@@ -39,7 +44,8 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
private _terminal: ITerminal,
|
private _terminal: ITerminal,
|
||||||
private _viewportElement: HTMLElement,
|
private _viewportElement: HTMLElement,
|
||||||
private _scrollArea: HTMLElement,
|
private _scrollArea: HTMLElement,
|
||||||
private _charMeasure: CharMeasure
|
private _charMeasure: CharMeasure,
|
||||||
|
private _dimensions: IRenderDimensions
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@@ -53,7 +59,11 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
setTimeout(() => this.syncScrollArea(), 0);
|
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;
|
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,40 +72,69 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
* necessary.
|
* necessary.
|
||||||
*/
|
*/
|
||||||
private _refresh(): void {
|
private _refresh(): void {
|
||||||
if (this._charMeasure.height > 0) {
|
if (this._refreshAnimationFrame === null) {
|
||||||
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
|
||||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
|
||||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
|
|
||||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
|
||||||
this._lastRecordedBufferHeight = newBufferHeight;
|
|
||||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private _innerRefresh(): void {
|
||||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
if (this._charMeasure.height > 0) {
|
||||||
*/
|
this._currentRowHeight = this._dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||||
public syncScrollArea(): void {
|
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||||
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._dimensions.canvasHeight);
|
||||||
// If buffer height changed
|
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||||
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
this._lastRecordedBufferHeight = newBufferHeight;
|
||||||
this._refresh();
|
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||||
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
|
||||||
// If viewport height changed
|
|
||||||
this._refresh();
|
|
||||||
} else {
|
|
||||||
// If size has changed, refresh viewport
|
|
||||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
|
||||||
this._refresh();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync scrollTop
|
// Sync scrollTop
|
||||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||||
|
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
|
||||||
|
// want this event to scroll the terminal
|
||||||
|
this._ignoreNextScrollEvent = true;
|
||||||
this._viewportElement.scrollTop = scrollTop;
|
this._viewportElement.scrollTop = scrollTop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._refreshAnimationFrame = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||||
|
*/
|
||||||
|
public syncScrollArea(): void {
|
||||||
|
// If buffer height changed
|
||||||
|
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
||||||
|
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If viewport height changed
|
||||||
|
if (this._lastRecordedViewportHeight !== this._dimensions.canvasHeight) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the buffer position doesn't match last scroll top
|
||||||
|
const newScrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||||
|
if (this._lastScrollTop !== newScrollTop) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If element's scroll top changed, this can happen when hiding the element
|
||||||
|
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If row height changed
|
||||||
|
if (this._dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||||
|
this._refresh();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,13 +143,22 @@ export class Viewport extends Disposable implements IViewport {
|
|||||||
* @param ev The scroll event.
|
* @param ev The scroll event.
|
||||||
*/
|
*/
|
||||||
private _onScroll(ev: Event): void {
|
private _onScroll(ev: Event): void {
|
||||||
|
// Record current scroll top position
|
||||||
|
this._lastScrollTop = this._viewportElement.scrollTop;
|
||||||
|
|
||||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||||
// which causes the terminal to scroll the buffer to the top
|
// which causes the terminal to scroll the buffer to the top
|
||||||
if (!this._viewportElement.offsetParent) {
|
if (!this._viewportElement.offsetParent) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
|
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
||||||
|
if (this._ignoreNextScrollEvent) {
|
||||||
|
this._ignoreNextScrollEvent = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
||||||
const diff = newRow - this._terminal.buffer.ydisp;
|
const diff = newRow - this._terminal.buffer.ydisp;
|
||||||
this._terminal.scrollLines(diff, true);
|
this._terminal.scrollLines(diff, true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
__socket?: WebSocket;
|
||||||
__attachSocketBuffer?: string;
|
__attachSocketBuffer?: string;
|
||||||
|
__dataListener?: IDisposable;
|
||||||
|
|
||||||
__getMessage?(ev: MessageEvent): void;
|
__getMessage?(ev: MessageEvent): void;
|
||||||
__flushBuffer?(): void;
|
__flushBuffer?(): void;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as attach from './attach';
|
import * as attach from './attach';
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
|||||||
const fileReader = new FileReader();
|
const fileReader = new FileReader();
|
||||||
|
|
||||||
fileReader.addEventListener('load', () => {
|
fileReader.addEventListener('load', () => {
|
||||||
str = myTextDecoder.decode(this.result);
|
str = myTextDecoder.decode(fileReader.result);
|
||||||
displayData(str);
|
displayData(str);
|
||||||
});
|
});
|
||||||
fileReader.readAsArrayBuffer(ev.data);
|
fileReader.readAsArrayBuffer(ev.data);
|
||||||
@@ -90,7 +90,8 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
|||||||
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
||||||
|
|
||||||
if (bidirectional) {
|
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)));
|
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 {
|
export function detach(term: Terminal, socket: WebSocket): void {
|
||||||
const addonTerminal = <IAttachAddonTerminal>term;
|
const addonTerminal = <IAttachAddonTerminal>term;
|
||||||
addonTerminal.off('data', addonTerminal.__sendData);
|
addonTerminal.__dataListener.dispose();
|
||||||
|
addonTerminal.__dataListener = undefined;
|
||||||
|
|
||||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/attach/",
|
"outDir": "../../../lib/addons/attach/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true
|
||||||
"preserveWatchOutput": true
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as fit from './fit';
|
import * as fit from './fit';
|
||||||
|
|
||||||
|
|||||||
@@ -39,8 +39,8 @@ export function proposeGeometry(term: Terminal): IGeometry {
|
|||||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
|
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
|
||||||
const geometry = {
|
const geometry = {
|
||||||
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
|
cols: Math.floor(availableWidth / (<any>term)._core._renderCoordinator.dimensions.actualCellWidth),
|
||||||
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
|
rows: Math.floor(availableHeight / (<any>term)._core._renderCoordinator.dimensions.actualCellHeight)
|
||||||
};
|
};
|
||||||
return geometry;
|
return geometry;
|
||||||
}
|
}
|
||||||
@@ -50,7 +50,7 @@ export function fit(term: Terminal): void {
|
|||||||
if (geometry) {
|
if (geometry) {
|
||||||
// Force a full render
|
// Force a full render
|
||||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
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);
|
term.resize(geometry.cols, geometry.rows);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es5"
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/fit/",
|
"outDir": "../../../lib/addons/fit/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as fullscreen from './fullscreen';
|
import * as fullscreen from './fullscreen';
|
||||||
|
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ import { Terminal } from 'xterm';
|
|||||||
* @param fullscreen Toggle fullscreen on (true) or off (false)
|
* @param fullscreen Toggle fullscreen on (true) or off (false)
|
||||||
*/
|
*/
|
||||||
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
export function toggleFullScreen(term: Terminal, fullscreen: boolean): void {
|
||||||
let fn: string;
|
let fn: (...tokens: string[]) => void;
|
||||||
|
|
||||||
if (typeof fullscreen === 'undefined') {
|
if (typeof fullscreen === 'undefined') {
|
||||||
fn = (term.element.classList.contains('fullscreen')) ? 'remove' : 'add';
|
fn = (term.element.classList.contains('fullscreen')) ?
|
||||||
|
term.element.classList.remove : term.element.classList.add;
|
||||||
} else if (!fullscreen) {
|
} else if (!fullscreen) {
|
||||||
fn = 'remove';
|
fn = term.element.classList.remove;
|
||||||
} else {
|
} else {
|
||||||
fn = 'add';
|
fn = term.element.classList.add;
|
||||||
}
|
}
|
||||||
|
|
||||||
term.element.classList[fn]('fullscreen');
|
fn = fn.bind(term.element.classList);
|
||||||
|
fn('fullscreen');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apply(terminalConstructor: typeof Terminal): void {
|
export function apply(terminalConstructor: typeof Terminal): void {
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es5"
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/fullscreen/",
|
"outDir": "../../../lib/addons/fullscreen/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -5,18 +5,28 @@
|
|||||||
|
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
|
|
||||||
// TODO: Don't rely on this private API
|
|
||||||
export interface ITerminalCore {
|
|
||||||
buffer: any;
|
|
||||||
selectionManager: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ISearchAddonTerminal extends Terminal {
|
export interface ISearchAddonTerminal extends Terminal {
|
||||||
__searchHelper?: ISearchHelper;
|
__searchHelper?: ISearchHelper;
|
||||||
_core: ITerminalCore;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISearchHelper {
|
export interface ISearchHelper {
|
||||||
findNext(term: string): boolean;
|
findNext(term: string, searchOptions: ISearchOptions): boolean;
|
||||||
findPrevious(term: string): boolean;
|
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
term: string;
|
||||||
|
col: number;
|
||||||
|
row: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,56 +3,92 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
|
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||||
|
import { IDisposable } from 'xterm';
|
||||||
|
|
||||||
interface ISearchResult {
|
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||||
term: string;
|
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||||
col: number;
|
|
||||||
row: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A class that knows how to search the terminal and how to display the results.
|
* A class that knows how to search the terminal and how to display the results.
|
||||||
*/
|
*/
|
||||||
export class SearchHelper implements ISearchHelper {
|
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) {
|
constructor(private _terminal: ISearchAddonTerminal) {
|
||||||
// TODO: Search for multiple instances on 1 line
|
this._destroyLinesCache = this._destroyLinesCache.bind(this);
|
||||||
// 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
|
|
||||||
// TODO: Support regex, case sensitivity, etc.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the next instance of the term, then scroll to and select it. If it
|
* Find the next instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term The search term.
|
||||||
|
* @param searchOptions Search options.
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
public findNext(term: string): boolean {
|
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||||
|
const {incremental} = searchOptions;
|
||||||
|
let result: ISearchResult;
|
||||||
|
|
||||||
if (!term || term.length === 0) {
|
if (!term || term.length === 0) {
|
||||||
|
this._terminal.clearSelection();
|
||||||
return false;
|
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.hasSelection()) {
|
||||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
|
||||||
// Start from the selection end if there is a selection
|
// Start from the selection end if there is a selection
|
||||||
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
|
this._initLinesCache();
|
||||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
|
||||||
result = this._findInLine(term, y);
|
// A row that has isWrapped = false
|
||||||
if (result) {
|
let findingRow = startRow;
|
||||||
break;
|
// 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 startRow
|
||||||
|
result = this._findInLine(term, findingRow, cumulativeCols, searchOptions);
|
||||||
|
|
||||||
|
// Search from startRow + 1 to end
|
||||||
|
if (!result) {
|
||||||
|
|
||||||
|
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 current ydisp
|
// Search from the top to the startRow (search the whole startRow again in
|
||||||
|
// case startCol > 0)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
for (let y = 0; y < startRow; y++) {
|
for (let y = 0; y < findingRow; y++) {
|
||||||
result = this._findInLine(term, y);
|
result = this._findInLine(term, y, 0, searchOptions);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -66,37 +102,72 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
/**
|
/**
|
||||||
* Find the previous instance of the term, then scroll to and select it. If it
|
* Find the previous instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term The search term.
|
||||||
|
* @param searchOptions Search options.
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
public findPrevious(term: string): boolean {
|
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||||
|
let result: ISearchResult;
|
||||||
|
|
||||||
if (!term || term.length === 0) {
|
if (!term || term.length === 0) {
|
||||||
|
this._terminal.clearSelection();
|
||||||
return false;
|
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.hasSelection()) {
|
||||||
if (this._terminal._core.selectionManager.selectionStart) {
|
// Start from the selection start if there is a selection
|
||||||
// Start from the selection end if there is a selection
|
const currentSelection = this._terminal.getSelectionPosition();
|
||||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
startRow = currentSelection.startRow;
|
||||||
|
startCol = currentSelection.startColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search from ydisp + 1 to end
|
this._initLinesCache();
|
||||||
for (let y = startRow - 1; y >= 0; y--) {
|
|
||||||
result = this._findInLine(term, y);
|
// Search startRow
|
||||||
if (result) {
|
result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch);
|
||||||
break;
|
|
||||||
|
// Search from startRow - 1 to top
|
||||||
|
if (!result) {
|
||||||
|
// 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 top to the current ydisp
|
// Search from the bottom to startRow (search the whole startRow again in
|
||||||
|
// case startCol > 0)
|
||||||
if (!result) {
|
if (!result) {
|
||||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1;
|
||||||
result = this._findInLine(term, y);
|
let cumulativeCols = this._terminal.cols;
|
||||||
|
for (let y = searchFrom; y >= startRow; y--) {
|
||||||
|
result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch);
|
||||||
if (result) {
|
if (result) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if (this._terminal.buffer.getLine(y).isWrapped) {
|
||||||
|
cumulativeCols += this._terminal.cols;
|
||||||
|
} else {
|
||||||
|
cumulativeCols = this._terminal.cols;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,38 +176,156 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches a line for a search term.
|
* Sets up a line cache with a ttl
|
||||||
* @param term Tne search term.
|
*/
|
||||||
* @param y The line to search.
|
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
|
||||||
|
* @param line entire string in which the potential whole word was found
|
||||||
|
* @param term the substring that starts at searchIndex
|
||||||
|
*/
|
||||||
|
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
|
||||||
|
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
|
||||||
|
* 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 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.
|
* @return The search result if it was found.
|
||||||
*/
|
*/
|
||||||
private _findInLine(term: string, y: number): ISearchResult {
|
protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult {
|
||||||
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
|
||||||
const lowerTerm = term.toLowerCase();
|
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||||
let searchIndex = lowerStringLine.indexOf(lowerTerm);
|
if (this._terminal.buffer.getLine(row).isWrapped) {
|
||||||
if (searchIndex >= 0) {
|
return;
|
||||||
const line = this._terminal._core.buffer.lines.get(y);
|
}
|
||||||
for (let i = 0; i < searchIndex; i++) {
|
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||||
const charData = line[i];
|
if (stringLine === void 0) {
|
||||||
|
stringLine = this.translateBufferLineToStringWithWrap(row, true);
|
||||||
|
if (this._linesCache) {
|
||||||
|
this._linesCache[row] = stringLine;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} 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.buffer.getLine(row);
|
||||||
|
|
||||||
|
for (let i = 0; i < resultIndex; i++) {
|
||||||
|
const cell = line.getCell(i);
|
||||||
// Adjust the searchIndex to normalize emoji into single chars
|
// 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) {
|
if (char.length > 1) {
|
||||||
searchIndex -= char.length - 1;
|
resultIndex -= char.length - 1;
|
||||||
}
|
}
|
||||||
// Adjust the searchIndex for empty characters following wide unicode
|
// Adjust the searchIndex for empty characters following wide unicode
|
||||||
// chars (eg. CJK)
|
// chars (eg. CJK)
|
||||||
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
|
const charWidth = cell.width;
|
||||||
if (charWidth === 0) {
|
if (charWidth === 0) {
|
||||||
searchIndex++;
|
resultIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
term,
|
term,
|
||||||
col: searchIndex,
|
col: resultIndex,
|
||||||
row: y
|
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
|
||||||
|
* function is useful for getting the actual text underneath the raw selection
|
||||||
|
* position.
|
||||||
|
* @param line The line being translated.
|
||||||
|
* @param trimRight Whether to trim whitespace to the right.
|
||||||
|
*/
|
||||||
|
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
|
||||||
|
let lineString = '';
|
||||||
|
let lineWrapsToNext: boolean;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const nextLine = this._terminal.buffer.getLine(lineIndex + 1);
|
||||||
|
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||||
|
lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols);
|
||||||
|
lineIndex++;
|
||||||
|
} while (lineWrapsToNext);
|
||||||
|
|
||||||
|
return lineString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and scrolls to a result.
|
* Selects and scrolls to a result.
|
||||||
@@ -145,10 +334,11 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
*/
|
*/
|
||||||
private _selectResult(result: ISearchResult): boolean {
|
private _selectResult(result: ISearchResult): boolean {
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
this._terminal.clearSelection();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
|
this._terminal.select(result.col, result.row, result.term.length);
|
||||||
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
|
this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,358 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
declare var require: any;
|
||||||
|
|
||||||
|
import { assert, expect } from 'chai';
|
||||||
|
import * as search from './search';
|
||||||
|
import { SearchHelper } from './SearchHelper';
|
||||||
|
import { ISearchOptions, ISearchResult } from './Interfaces';
|
||||||
|
|
||||||
|
class MockTerminalPlain {}
|
||||||
|
|
||||||
|
class MockTerminal {
|
||||||
|
private _core: any;
|
||||||
|
public searchHelper: TestSearchHelper;
|
||||||
|
public cols: number;
|
||||||
|
constructor(options: any) {
|
||||||
|
this._core = new (require('../../../lib/Terminal')).Terminal(options);
|
||||||
|
this.searchHelper = new TestSearchHelper(this as any);
|
||||||
|
this.cols = options.cols;
|
||||||
|
}
|
||||||
|
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, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('search addon', () => {
|
||||||
|
describe('apply', () => {
|
||||||
|
it('should register findNext and findPrevious', () => {
|
||||||
|
search.apply(<any>MockTerminalPlain);
|
||||||
|
assert.equal(typeof (<any>MockTerminalPlain).prototype.findNext, 'function');
|
||||||
|
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('find', () => {
|
||||||
|
it('Searchhelper - should find correct position', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
|
term.core.write('Hello World\r\ntest\n123....hello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should find search term accross line wrap', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 10, rows: 5});
|
||||||
|
term.core.write('texttextHellotext\r\n');
|
||||||
|
term.core.write('texttexttextHellotext goodbye');
|
||||||
|
term.pushWriteData();
|
||||||
|
/*
|
||||||
|
texttextHe
|
||||||
|
llotext
|
||||||
|
texttextte
|
||||||
|
xtHellotex
|
||||||
|
t (these spaces included intentionally)
|
||||||
|
goodbye
|
||||||
|
*/
|
||||||
|
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello', 3);
|
||||||
|
const llo = term.searchHelper.findInLine('llo', 1);
|
||||||
|
const goodbye = term.searchHelper.findInLine('goodbye', 2);
|
||||||
|
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
expect(llo).eql(undefined);
|
||||||
|
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
|
||||||
|
term.core.resize(9, 5);
|
||||||
|
const hello0Resize = term.searchHelper.findInLine('Hello', 0);
|
||||||
|
expect(hello0Resize).eql({col: 8, row: 0, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should respect search regex', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 10, rows: 4});
|
||||||
|
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
|
||||||
|
/*
|
||||||
|
abcdefghij
|
||||||
|
klmnopqrst
|
||||||
|
uvwxyz
|
||||||
|
~/dev
|
||||||
|
*/
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
|
||||||
|
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
|
||||||
|
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
|
||||||
|
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
|
||||||
|
expect(hello0).eql({col: 3, row: 0, term: 'de'});
|
||||||
|
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(tilda0).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda1).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda2).eql({col: 0, row: 3, term: '~'});
|
||||||
|
});
|
||||||
|
it('should not select empty lines', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
|
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||||
|
expect(line).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect case sensitive', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 4});
|
||||||
|
term.core.write('Hello World\r\n123....hello\r\nmoreTestHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 8, row: 2, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should respect case sensitive + regex', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 4});
|
||||||
|
term.core.write('hellohello\r\nHelloHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* hellohello
|
||||||
|
* HelloHello
|
||||||
|
*/
|
||||||
|
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql(undefined);
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql({col: 0, row: 1, term: 'Hello'});
|
||||||
|
expect(hello3).eql({col: 5, row: 1, term: 'Hello'});
|
||||||
|
});
|
||||||
|
it('should respect whole-word search option', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions);
|
||||||
|
const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql({col: 6, row: 1, term: 'Hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
expect(hello4).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect whole-word + case sensitive search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World\r\nHelloWorld');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: false,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql(undefined);
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect whole-word + regex search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'hello'});
|
||||||
|
expect(hello1).eql({col: 12, row: 0, term: 'hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
});
|
||||||
|
it('should respect all search options', function(): void {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 5});
|
||||||
|
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: true,
|
||||||
|
caseSensitive: true
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||||
|
const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions);
|
||||||
|
const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||||
|
const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions);
|
||||||
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
|
expect(hello1).eql({col: 12, row: 0, term: 'Hello'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(hello3).eql(undefined);
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
+13
-11
@@ -5,42 +5,44 @@
|
|||||||
|
|
||||||
import { SearchHelper } from './SearchHelper';
|
import { SearchHelper } from './SearchHelper';
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { ISearchAddonTerminal } from './Interfaces';
|
import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the next instance of the term, then scroll to and select it. If it
|
* Find the next instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term The search term.
|
||||||
|
* @param searchOptions Search options
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
export function findNext(terminal: Terminal, term: string): boolean {
|
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {
|
||||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||||
if (!addonTerminal.__searchHelper) {
|
if (!addonTerminal.__searchHelper) {
|
||||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||||
}
|
}
|
||||||
return addonTerminal.__searchHelper.findNext(term);
|
return addonTerminal.__searchHelper.findNext(term, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the previous instance of the term, then scroll to and select it. If it
|
* Find the previous instance of the term, then scroll to and select it. If it
|
||||||
* doesn't exist, do nothing.
|
* doesn't exist, do nothing.
|
||||||
* @param term Tne search term.
|
* @param term The search term.
|
||||||
|
* @param searchOptions Search options
|
||||||
* @return Whether a result was found.
|
* @return Whether a result was found.
|
||||||
*/
|
*/
|
||||||
export function findPrevious(terminal: Terminal, term: string): boolean {
|
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {
|
||||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||||
if (!addonTerminal.__searchHelper) {
|
if (!addonTerminal.__searchHelper) {
|
||||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||||
}
|
}
|
||||||
return addonTerminal.__searchHelper.findPrevious(term);
|
return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function apply(terminalConstructor: typeof Terminal): void {
|
export function apply(terminalConstructor: typeof Terminal): void {
|
||||||
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
|
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||||
return findNext(this, term);
|
return findNext(this, term, searchOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
|
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||||
return findPrevious(this, term);
|
return findPrevious(this, term, searchOptions);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es5"
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/search/",
|
"outDir": "../../../lib/addons/search/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -5,11 +5,16 @@
|
|||||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
* 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 {
|
export interface ITerminadoAddonTerminal extends Terminal {
|
||||||
|
_core: {
|
||||||
|
register<T extends IDisposable>(d: T): void;
|
||||||
|
};
|
||||||
|
|
||||||
__socket?: WebSocket;
|
__socket?: WebSocket;
|
||||||
__attachSocketBuffer?: string;
|
__attachSocketBuffer?: string;
|
||||||
|
__dataListener?: IDisposable;
|
||||||
|
|
||||||
__getMessage?(ev: MessageEvent): void;
|
__getMessage?(ev: MessageEvent): void;
|
||||||
__flushBuffer?(): void;
|
__flushBuffer?(): void;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as terminado from './terminado';
|
import * as terminado from './terminado';
|
||||||
|
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ export function terminadoAttach(term: Terminal, socket: WebSocket, bidirectional
|
|||||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||||
|
|
||||||
if (bidirectional) {
|
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('close', () => terminadoDetach(addonTerminal, socket));
|
||||||
socket.addEventListener('error', () => 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 {
|
export function terminadoDetach(term: Terminal, socket: WebSocket): void {
|
||||||
const addonTerminal = <ITerminadoAddonTerminal>term;
|
const addonTerminal = <ITerminadoAddonTerminal>term;
|
||||||
addonTerminal.off('data', addonTerminal.__sendData);
|
addonTerminal.__dataListener.dispose();
|
||||||
|
addonTerminal.__dataListener = undefined;
|
||||||
|
|
||||||
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket;
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es5"
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/terminado/",
|
"outDir": "../../../lib/addons/terminado/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es5",
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/webLinks/",
|
"outDir": "../../../lib/addons/webLinks/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as webLinks from './webLinks';
|
import * as webLinks from './webLinks';
|
||||||
|
|
||||||
@@ -28,15 +28,185 @@ describe('webLinks addon', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow ~ character in URI path', () => {
|
describe('should allow simple URI path', () => {
|
||||||
const term = new MockTerminal();
|
it('foo.com', () => {
|
||||||
webLinks.webLinksInit(<any>term);
|
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 match = row.match(term.regex);
|
||||||
const uri = match[term.options.matchIndex];
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should allow ~ character in 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 match = row.match(term.regex);
|
||||||
|
const uri = match[term.options.matchIndex];
|
||||||
|
|
||||||
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 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 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 localHostClause = '(localhost)';
|
||||||
const portClause = '(:\\d{1,5})';
|
const portClause = '(:\\d{1,5})';
|
||||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||||
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])';
|
||||||
|
const pathClause = '(' + pathCharacterSet + ')?';
|
||||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
const hashFragmentClause = '(#' + 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,16 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"module": "commonjs",
|
|
||||||
"target": "es5",
|
|
||||||
"rootDir": ".",
|
|
||||||
"outDir": "../../../lib/addons/winptyCompat/",
|
|
||||||
"sourceMap": true,
|
|
||||||
"removeComments": true,
|
|
||||||
"declaration": true,
|
|
||||||
"preserveWatchOutput": true
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
"../../../typings/xterm.d.ts"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { assert, expect } 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,43 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
|
||||||
|
|
||||||
export function winptyCompatInit(terminal: Terminal): void {
|
|
||||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
|
||||||
|
|
||||||
// Don't do anything when the platform is not Windows
|
|
||||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
|
||||||
if (!isWindows) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Winpty does not support wraparound mode which means that lines will never
|
|
||||||
// be marked as wrapped. This causes issues for things like copying a line
|
|
||||||
// retaining the wrapped new line characters or if consumers are listening
|
|
||||||
// in on the data stream.
|
|
||||||
//
|
|
||||||
// The workaround for this is to listen to every incoming line feed and mark
|
|
||||||
// the line as wrapped if the last character in the previous line is not a
|
|
||||||
// space. This is certainly not without its problems, but generally on
|
|
||||||
// Windows when text reaches the end of the terminal it's likely going to be
|
|
||||||
// wrapped.
|
|
||||||
addonTerminal.on('linefeed', () => {
|
|
||||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
|
||||||
const lastChar = line[addonTerminal.cols - 1];
|
|
||||||
|
|
||||||
if (lastChar[3] !== 32 /* ' ' */) {
|
|
||||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
|
||||||
(<any>nextLine).isWrapped = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function apply(terminalConstructor: typeof Terminal): void {
|
|
||||||
(<any>terminalConstructor.prototype).winptyCompatInit = function (): void {
|
|
||||||
winptyCompatInit(this);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,17 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es5"
|
||||||
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../../../lib/addons/zmodem/",
|
"outDir": "../../../lib/addons/zmodem/",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"preserveWatchOutput": true
|
"types": [
|
||||||
|
"../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert, expect } from 'chai';
|
import { assert } from 'chai';
|
||||||
|
|
||||||
import * as zmodem from './zmodem';
|
import * as zmodem from './zmodem';
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import { Terminal } from 'xterm';
|
|||||||
* via `detach()` and a re-`attach()`.)
|
* via `detach()` and a re-`attach()`.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let zmodem;
|
let zmodem: any;
|
||||||
|
|
||||||
export interface IZmodemOptions {
|
export interface IZmodemOptions {
|
||||||
noTerminalWriteOutsideSession?: boolean;
|
noTerminalWriteOutsideSession?: boolean;
|
||||||
@@ -44,7 +44,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
|
|||||||
const term = this;
|
const term = this;
|
||||||
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
|
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
|
||||||
|
|
||||||
let zsentry;
|
let zsentry: any;
|
||||||
|
|
||||||
function shouldWrite(): boolean {
|
function shouldWrite(): boolean {
|
||||||
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
|
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user