Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a61642b81 | ||
|
|
0a0ebb68b5 | ||
|
|
e77a0ec886 | ||
|
|
1bf2ccc572 | ||
|
|
5f680c2185 | ||
|
|
38450082ac | ||
|
|
1801403ce2 | ||
|
|
4c7d89e67d | ||
|
|
5d3a9e864d | ||
|
|
911d92200a | ||
|
|
1a788f6363 | ||
|
|
a354f36e6f | ||
|
|
343e1c8a86 | ||
|
|
783722c2f3 | ||
|
|
44e4b56ced | ||
|
|
52044e9aad | ||
|
|
4cdf308c2f | ||
|
|
d24a27d93e | ||
|
|
55beec8b96 | ||
|
|
81dce7a4ba | ||
|
|
ddf3f08a6c | ||
|
|
8c6140c2e3 | ||
|
|
1639c0a031 | ||
|
|
fb950fcbe5 | ||
|
|
1bb465d6b0 | ||
|
|
efb939d171 | ||
|
|
fe99422ca4 | ||
|
|
d28819de41 | ||
|
|
f8e9f1813a | ||
|
|
48b8e31794 | ||
|
|
58b657bc3b | ||
|
|
bc9c9dd6fd | ||
|
|
6b16732192 | ||
|
|
5aaa8dafc5 | ||
|
|
7a652c4169 | ||
|
|
1980f675f9 | ||
|
|
98c40e3f8e | ||
|
|
6ced3014cf | ||
|
|
3e8aecaea3 | ||
|
|
e3c008ba38 | ||
|
|
d2083c5e8c | ||
|
|
0c0a34d888 | ||
|
|
cd975d22cc | ||
|
|
b5f53b341d | ||
|
|
221db55680 | ||
|
|
5cc1114594 | ||
|
|
6ad553483d | ||
|
|
49872f9377 | ||
|
|
1e17ba7a80 | ||
|
|
1c8b08d55c | ||
|
|
ca083f257c | ||
|
|
8b53b8c255 | ||
|
|
ba078ca1f9 | ||
|
|
4ededf5b06 | ||
|
|
384cd9db30 | ||
|
|
55b420b016 | ||
|
|
ddd43e0f09 | ||
|
|
41fdb96b68 | ||
|
|
e5fb255b79 | ||
|
|
7f14bfc7d9 | ||
|
|
8f97a5a786 | ||
|
|
5353808437 | ||
|
|
38f8bc5f3c | ||
|
|
48c21dc3ae | ||
|
|
bcb71e854f | ||
|
|
e3c361327c | ||
|
|
51ef27894e | ||
|
|
7edc11bd04 | ||
|
|
ebe55e5b5f | ||
|
|
3d541dd323 | ||
|
|
53f05de3e7 | ||
|
|
d1a87facfd | ||
|
|
18a2751471 | ||
|
|
59e1bddb6e | ||
|
|
d6ad4a4fc3 | ||
|
|
f9bd0b8fed | ||
|
|
ec26820ea6 | ||
|
|
d7fdf17eab | ||
|
|
a5f2ea336e | ||
|
|
de5df69333 | ||
|
|
f2b90d690f | ||
|
|
eec53261e0 | ||
|
|
eb96187801 | ||
|
|
c502272373 | ||
|
|
87bc48f5bc | ||
|
|
d7fb0141d2 | ||
|
|
2fa125904a | ||
|
|
de03391c2d | ||
|
|
a38e97f1ec | ||
|
|
5d98304249 | ||
|
|
82a6144fd1 | ||
|
|
995080c40c | ||
|
|
5ad12dff8d | ||
|
|
a458bae36d | ||
|
|
900200b871 | ||
|
|
77d5d52e71 | ||
|
|
1a50cfabe0 | ||
|
|
3abbaa9ba9 | ||
|
|
5a9f2635d3 | ||
|
|
d24c473172 | ||
|
|
da93a35671 | ||
|
|
7d4b61413e | ||
|
|
13e33a8da4 | ||
|
|
ede3d10528 | ||
|
|
64ea96cc13 | ||
|
|
47e3dc7bcd | ||
|
|
748d0cc74f | ||
|
|
343ea9bcd0 | ||
|
|
cd01157e55 | ||
|
|
4cc4b2cd42 | ||
|
|
29c1264b42 | ||
|
|
d84b71d4c6 | ||
|
|
f18528d2bb | ||
|
|
ddde738326 | ||
|
|
24919e36f0 | ||
|
|
f5223d30b8 | ||
|
|
7b4423819e | ||
|
|
372c2970e6 | ||
|
|
bc4158c9b8 | ||
|
|
8164c638e5 | ||
|
|
8b9fa324f4 | ||
|
|
0976b15709 | ||
|
|
181fb1cfe5 | ||
|
|
09aed4d339 | ||
|
|
aeb4fa0fe9 | ||
|
|
0ae5aedcf9 | ||
|
|
3d41f028e4 | ||
|
|
324ea2ecb7 | ||
|
|
999f255893 | ||
|
|
931ee9e89e | ||
|
|
c0b4edaf31 | ||
|
|
abdce874db | ||
|
|
edba006045 | ||
|
|
6b4df216d3 | ||
|
|
f502f55ae1 | ||
|
|
afe1d7ed6f | ||
|
|
1628795557 | ||
|
|
e9643f5936 | ||
|
|
ba61509fdd | ||
|
|
a67e7286d3 | ||
|
|
b85f5aacd6 | ||
|
|
de574a27fc | ||
|
|
7c87f5b47a | ||
|
|
0a26cee08b | ||
|
|
40716b8ca5 | ||
|
|
d29388394c | ||
|
|
714d4b1cd6 | ||
|
|
9f1db12b1b | ||
|
|
56c7c5c4ed | ||
|
|
13abd2aa96 | ||
|
|
10598462d0 | ||
|
|
0c6877f46f | ||
|
|
0c4ee0db65 | ||
|
|
5d09d3b217 | ||
|
|
63eb9a4555 | ||
|
|
b7c0332626 | ||
|
|
95979c4a7d | ||
|
|
5019e14753 | ||
|
|
85e0625078 | ||
|
|
873e3745ec | ||
|
|
9b19187307 | ||
|
|
218745ab85 | ||
|
|
1687d06f1c | ||
|
|
261209cab6 | ||
|
|
bb55cd2881 | ||
|
|
7b38283db0 | ||
|
|
908e84a1f8 | ||
|
|
656912c934 | ||
|
|
7fa32af051 | ||
|
|
fa92e6074d | ||
|
|
239f669041 | ||
|
|
214555085a | ||
|
|
da33543ffb | ||
|
|
e643286cd5 | ||
|
|
38f0dd9be2 | ||
|
|
6d801311cd | ||
|
|
3bd37115d1 | ||
|
|
0785a1bf24 | ||
|
|
413a2b22fa | ||
|
|
e98b6315e4 | ||
|
|
d9a33d080b | ||
|
|
85b5161380 | ||
|
|
2b916a24aa | ||
|
|
ccaf0a71c6 | ||
|
|
b3fd901699 | ||
|
|
a32e3958d8 | ||
|
|
dbd999c4a4 | ||
|
|
8158a16629 | ||
|
|
7eace4d479 | ||
|
|
e98a20ba9e | ||
|
|
d072a628ca | ||
|
|
6b26a1bcf8 | ||
|
|
8524c23deb | ||
|
|
7c5853bf00 | ||
|
|
23afd53301 | ||
|
|
1d728f7f87 | ||
|
|
5030dc3d6a | ||
|
|
47fe7b2318 | ||
|
|
51d18e533f | ||
|
|
842f4c2088 | ||
|
|
c8b0ddbc72 | ||
|
|
4319df0eeb | ||
|
|
bd3bd7fda8 | ||
|
|
7f87b46328 | ||
|
|
32ae29be37 | ||
|
|
89af5b1098 | ||
|
|
5aaed8cf0b | ||
|
|
06f3f8f267 | ||
|
|
14d22a2002 | ||
|
|
4337326169 | ||
|
|
28dcd38a53 | ||
|
|
bb2367ab4a | ||
|
|
02795f8803 | ||
|
|
8528e8e83b | ||
|
|
1f690c7703 | ||
|
|
24fb498757 | ||
|
|
7836097e56 | ||
|
|
86f80b344b | ||
|
|
49bcb969b6 | ||
|
|
b55c7e342a | ||
|
|
23ed3327ce | ||
|
|
d57329f7e5 | ||
|
|
f6a384b401 | ||
|
|
79c990d73e | ||
|
|
ecfc2d4b40 | ||
|
|
becab08b6a | ||
|
|
8ad0bd1638 | ||
|
|
544c10009c | ||
|
|
3457fb4872 | ||
|
|
8e17cace6f | ||
|
|
93f7ad86da | ||
|
|
3a0f64545e | ||
|
|
5af5fe319b | ||
|
|
b9d10f1453 | ||
|
|
14bfc0bf0e | ||
|
|
078d88fbbe | ||
|
|
dfe35a93d5 | ||
|
|
7ff27240f0 | ||
|
|
550b19f334 | ||
|
|
74409da947 | ||
|
|
1f60a398b2 | ||
|
|
21e1c5ef58 | ||
|
|
dddde8ec23 | ||
|
|
43a014e8c1 | ||
|
|
b39477d81a | ||
|
|
6ef1bdd7ce | ||
|
|
f9deb2f840 | ||
|
|
23076ba6cc | ||
|
|
fe530d92ec | ||
|
|
efda0e77a9 | ||
|
|
87dd275a51 | ||
|
|
ef3695f727 | ||
|
|
09d1a01126 | ||
|
|
7a70db709c | ||
|
|
7f131aa19a | ||
|
|
4645ef706e | ||
|
|
866ee23df9 | ||
|
|
fefdd8a4e9 | ||
|
|
7a502c6b83 | ||
|
|
90dca7d650 | ||
|
|
e8aa4eba75 | ||
|
|
64d5af38f5 | ||
|
|
4cea6b3f41 | ||
|
|
b9331ec810 | ||
|
|
97e4af3707 | ||
|
|
faa3769ed1 | ||
|
|
1098173908 | ||
|
|
8ff34a6aee | ||
|
|
bec4cb37d4 | ||
|
|
12a999dfe0 | ||
|
|
8225ecd3ba | ||
|
|
31c11c7a65 | ||
|
|
a0aae62cb7 | ||
|
|
e79c21d4b2 | ||
|
|
61e5c10fd3 | ||
|
|
fef30bdba4 | ||
|
|
61c2ca71d8 | ||
|
|
8328fcac52 | ||
|
|
dd503785be | ||
|
|
85683a457d | ||
|
|
15c2432b6f | ||
|
|
66dcd1694c | ||
|
|
8e68c67a0c | ||
|
|
d2913d607d | ||
|
|
01fd148673 | ||
|
|
47ec31361c | ||
|
|
39d1546753 | ||
|
|
b5f208294d | ||
|
|
77622f8919 | ||
|
|
816220ebd2 | ||
|
|
4a661da57b | ||
|
|
f43afde8df | ||
|
|
6c4f8b42dd | ||
|
|
a1468d0f58 | ||
|
|
c8dc679086 | ||
|
|
9bc5090bac | ||
|
|
29e9685839 | ||
|
|
02e2727fb5 | ||
|
|
a05cdbd48e | ||
|
|
8f1dcecb50 | ||
|
|
bb2193676b | ||
|
|
f5b5716582 | ||
|
|
38ccf1d5c7 | ||
|
|
ba4252bc0b | ||
|
|
900d0132cb | ||
|
|
5a6a275a53 | ||
|
|
731432a852 | ||
|
|
c70be89bc2 | ||
|
|
03bc29e0ed | ||
|
|
eb562b5d89 | ||
|
|
d60431f4bf | ||
|
|
de56d04ae5 | ||
|
|
c2145d5125 | ||
|
|
90f64ba6cc | ||
|
|
26f8fce538 | ||
|
|
19399066f1 | ||
|
|
f38d7d86ac | ||
|
|
450ee0c6a5 | ||
|
|
6899f9dc4d | ||
|
|
f92d1f46a6 | ||
|
|
12efc1a455 | ||
|
|
f4593b76df | ||
|
|
9e8e6159cf | ||
|
|
b5d0a46ccb | ||
|
|
fb13f812f7 | ||
|
|
6162ae9ffe | ||
|
|
b6e1925ba8 | ||
|
|
fd3b22e8ab | ||
|
|
a4212e98d2 | ||
|
|
e293d29fd3 | ||
|
|
1a9965ef01 | ||
|
|
05eb12e6dd | ||
|
|
40b8854df4 | ||
|
|
1bab173e99 | ||
|
|
f678f0824b | ||
|
|
8b32d0c8ba | ||
|
|
8b55497adf | ||
|
|
3870507305 | ||
|
|
a0a7abbf49 | ||
|
|
c277fb742d | ||
|
|
7f8592282f | ||
|
|
963ed59459 | ||
|
|
22d889ab07 | ||
|
|
a4f830aa5d | ||
|
|
6f5008dba6 | ||
|
|
5777fbc608 | ||
|
|
035a7f710e | ||
|
|
d92e636482 | ||
|
|
b05db37ac1 | ||
|
|
53c432df27 | ||
|
|
8ccbef850e | ||
|
|
19b212d570 | ||
|
|
a9a6920b13 | ||
|
|
9f6d4f52f3 | ||
|
|
e7baccdbcd | ||
|
|
4cc0db4273 | ||
|
|
3e283e9168 | ||
|
|
a39facfe28 | ||
|
|
1c38f859b7 | ||
|
|
cee2411fb4 | ||
|
|
fdc2bc4960 | ||
|
|
b146ffd30f | ||
|
|
45ecc837e4 | ||
|
|
5b8777267a | ||
|
|
8488f1ad63 | ||
|
|
a93f81c270 | ||
|
|
21263a5f60 | ||
|
|
1905d7d8ca | ||
|
|
adaee5e495 | ||
|
|
71649f5e40 | ||
|
|
bd21c6afa5 | ||
|
|
df929f2dd5 | ||
|
|
e7e9d5ca4c | ||
|
|
80e7963dbe | ||
|
|
f98b4ce1ba | ||
|
|
520d5dd9eb | ||
|
|
4ef5f6ae86 | ||
|
|
39f9c3507b | ||
|
|
3e47c96445 | ||
|
|
079bbc8242 | ||
|
|
bfb048110f | ||
|
|
fc09730ffb | ||
|
|
3dc4f1a7f2 | ||
|
|
65b9a6cfc4 | ||
|
|
4b615f48ff | ||
|
|
c8d1266f0a | ||
|
|
7dc3332b05 | ||
|
|
ced06629d4 | ||
|
|
3b279e4a4d | ||
|
|
9e598d9fba | ||
|
|
f025c0cf4e | ||
|
|
8adf9473b7 | ||
|
|
692df151e5 | ||
|
|
4022277cec | ||
|
|
1250ab637a | ||
|
|
a14248d710 | ||
|
|
98a03dd477 | ||
|
|
2f85448e51 | ||
|
|
b4de2787c7 | ||
|
|
e7bbc4151a | ||
|
|
453cb00807 | ||
|
|
da2c59d1d0 | ||
|
|
372b6b633b | ||
|
|
fa4479c797 | ||
|
|
96574425c7 | ||
|
|
60c11e87f6 | ||
|
|
bdfc82d1b7 | ||
|
|
167a6fdbf6 | ||
|
|
89a0cee5a4 | ||
|
|
724bbebc14 | ||
|
|
e797070121 | ||
|
|
cfd7912afa | ||
|
|
1948ec886f | ||
|
|
249e73cd25 | ||
|
|
89112fb1ae | ||
|
|
d46a685925 | ||
|
|
785a8190a6 | ||
|
|
3a499fc35d | ||
|
|
4350c7ffcd | ||
|
|
a5a9c3b368 | ||
|
|
19367a6042 | ||
|
|
2263a1579e | ||
|
|
622a5bb9c3 | ||
|
|
82706e77db | ||
|
|
e75b47ca01 | ||
|
|
1af22b24c0 | ||
|
|
85dd6a9e87 | ||
|
|
7bf12f9123 | ||
|
|
798b805fe0 | ||
|
|
f5b66775db | ||
|
|
ff6fa739c4 | ||
|
|
4799832a33 | ||
|
|
c10a451381 | ||
|
|
78c2f06107 | ||
|
|
1e8901630c | ||
|
|
f4da8af2e9 | ||
|
|
83cb7c3184 | ||
|
|
a8fbc8d7a3 | ||
|
|
c5a48043ae | ||
|
|
9b02e950f8 | ||
|
|
14366e7c40 | ||
|
|
e1c149521a | ||
|
|
f5ccf75781 | ||
|
|
771d5f580a | ||
|
|
336b45a645 | ||
|
|
0f15a78ba2 | ||
|
|
69d6d53631 | ||
|
|
2c76f9bee7 | ||
|
|
0314d95a32 | ||
|
|
d06a2e1ad7 | ||
|
|
134e1ebb5f | ||
|
|
3c5b73df8e | ||
|
|
ef0312473e | ||
|
|
7003c24126 | ||
|
|
34ec80a322 | ||
|
|
ded6750ff5 | ||
|
|
d2e34277e8 | ||
|
|
e8579bd655 | ||
|
|
a4f5cbf0cc | ||
|
|
b6937d05cd | ||
|
|
56b824d379 | ||
|
|
72f7e3fac3 | ||
|
|
d90bbdaa8a | ||
|
|
8df23582d2 | ||
|
|
e5459d7e24 | ||
|
|
b3d9104136 | ||
|
|
912284c24c | ||
|
|
dd86776c6c | ||
|
|
44bc9a1f58 | ||
|
|
7b9d775a97 | ||
|
|
1da492646c | ||
|
|
e7743c4baf | ||
|
|
cd30f54276 | ||
|
|
a923953373 | ||
|
|
c16b45cc1c | ||
|
|
365a07968b | ||
|
|
da5c24a33f | ||
|
|
92a61e6506 | ||
|
|
e08962b8b8 | ||
|
|
f839a0da8b | ||
|
|
1efdc2e4ce | ||
|
|
a4fee6d4b5 | ||
|
|
ed24e952f8 | ||
|
|
5b44deaa30 | ||
|
|
db1fd267a6 | ||
|
|
816f62cebc | ||
|
|
b8e003768d | ||
|
|
7134eaf20c | ||
|
|
b5477f71a8 | ||
|
|
f42e78d832 | ||
|
|
9847dcaacc | ||
|
|
d943aa3e5b | ||
|
|
8098fd8d01 | ||
|
|
4ca26f7e7b | ||
|
|
4da2db8e0b | ||
|
|
8dfed8f979 | ||
|
|
e720643315 | ||
|
|
40e6dba43c | ||
|
|
7d357b941f | ||
|
|
3f525fb960 | ||
|
|
de3a09e05f | ||
|
|
d8573e38a8 | ||
|
|
e44659e8bd | ||
|
|
f160436df8 | ||
|
|
06357b1184 | ||
|
|
d64cbc41d5 | ||
|
|
71bfbe434b | ||
|
|
9ed95206c1 | ||
|
|
14b71b611e | ||
|
|
881bc4f0a8 | ||
|
|
b3b9f3c7cd | ||
|
|
74246497a4 | ||
|
|
bf5fed083b | ||
|
|
c7df6e3c2e | ||
|
|
6a79017484 | ||
|
|
e5d4e96aad | ||
|
|
93bfc18237 | ||
|
|
87675649e1 | ||
|
|
e2a402d80e | ||
|
|
3e204584d9 | ||
|
|
95c9fe11fb | ||
|
|
313cb2fe09 | ||
|
|
8e75cecba3 | ||
|
|
a2721d43fd | ||
|
|
db9bdd46b4 | ||
|
|
591f32d3eb | ||
|
|
fb4b19a393 | ||
|
|
ae958c2b12 | ||
|
|
216849b135 | ||
|
|
cfe306fa32 | ||
|
|
a9b496f06f | ||
|
|
f2991a5636 | ||
|
|
53606820a7 | ||
|
|
ad9818fb1a | ||
|
|
88bd33f2d7 | ||
|
|
5fd7216bf2 | ||
|
|
a9722371e1 | ||
|
|
58c2ba5310 | ||
|
|
d8234affa4 | ||
|
|
571bf9e533 | ||
|
|
723e915895 | ||
|
|
3e5ecbf52b | ||
|
|
01b66b756f | ||
|
|
275ae6b2d3 | ||
|
|
094763ea8b | ||
|
|
3299315822 | ||
|
|
10d2e66137 | ||
|
|
19b9d0eadd | ||
|
|
cae9f44f5c | ||
|
|
8b540c5f3b | ||
|
|
8bf3cd1102 | ||
|
|
b226eb2d64 | ||
|
|
e96b709d49 | ||
|
|
ac6877aba7 | ||
|
|
a13f11adb4 | ||
|
|
494aef1c94 | ||
|
|
10adc87a3e | ||
|
|
4d7d666c71 | ||
|
|
ac7486856c | ||
|
|
8999d3948a | ||
|
|
f3bd1e24b7 | ||
|
|
e9913522ac | ||
|
|
8f7db4b270 | ||
|
|
3745496d73 | ||
|
|
a64d352d48 | ||
|
|
42d403807c | ||
|
|
b2f541d4a6 | ||
|
|
912c33ca67 | ||
|
|
4c1722da2c | ||
|
|
4d70606feb | ||
|
|
16257386b1 | ||
|
|
a63890e627 | ||
|
|
904b491521 | ||
|
|
fea1079b1e | ||
|
|
b39d245050 | ||
|
|
b557ebe0a2 | ||
|
|
bb44dbfc51 | ||
|
|
490bf81b86 | ||
|
|
c3261beaea | ||
|
|
345b8d910b | ||
|
|
922ee7c48d | ||
|
|
ad59db9a8a | ||
|
|
49085bbd5b | ||
|
|
93ac9efe92 | ||
|
|
bc9e6b49e8 | ||
|
|
adedfed2bd | ||
|
|
9473777e43 | ||
|
|
a359647683 | ||
|
|
09a77998ba | ||
|
|
5ccb41b9b9 | ||
|
|
131e11b52b | ||
|
|
7d7726817a | ||
|
|
fc6245122d | ||
|
|
c5a0711f84 | ||
|
|
5a3e0612a7 | ||
|
|
b246318090 | ||
|
|
d807bdac5e | ||
|
|
1695baa5b0 | ||
|
|
9b4801fb43 | ||
|
|
d7854bc154 | ||
|
|
f9ac540031 | ||
|
|
00ce85166e | ||
|
|
6dc813bdff | ||
|
|
ebdd07b8ae | ||
|
|
c5810e6d54 | ||
|
|
c71252cd23 | ||
|
|
6c18b7f2cc | ||
|
|
5e77ad5cb4 | ||
|
|
e1578813a7 | ||
|
|
21811cb37d | ||
|
|
265227edcb | ||
|
|
1d3c1964f9 | ||
|
|
58b7a4a2d7 | ||
|
|
a6b0734fe8 | ||
|
|
55cd34c7a5 | ||
|
|
0e8aeebedb | ||
|
|
1c4d41db86 | ||
|
|
63c281e405 | ||
|
|
f03d0858cc | ||
|
|
8bdeb14bc6 | ||
|
|
ad7fe8f881 | ||
|
|
1dcc1d1baf | ||
|
|
0029bc3b42 | ||
|
|
0b73a8d469 | ||
|
|
3a2b97e0f7 | ||
|
|
9bcc98beb9 | ||
|
|
1fb11707a5 | ||
|
|
84548d5d7b | ||
|
|
6f666f56c1 | ||
|
|
2866977941 | ||
|
|
c087592e2c | ||
|
|
65182176b9 | ||
|
|
c8123d911e | ||
|
|
cacda99707 | ||
|
|
10e4d075a3 | ||
|
|
35c2d1ac4a | ||
|
|
86ad5198ac | ||
|
|
9a3ee49160 | ||
|
|
3da594abb0 | ||
|
|
72308b4f7f | ||
|
|
c22b3a92d7 | ||
|
|
f685360707 | ||
|
|
0fcd87085b | ||
|
|
8d2e4328ca | ||
|
|
ae6f5754f2 | ||
|
|
086ca58ece | ||
|
|
e635b3d507 | ||
|
|
df548e1106 | ||
|
|
13b8c78947 | ||
|
|
3cc9994a7d | ||
|
|
bac4eba35e | ||
|
|
625df61489 | ||
|
|
ccca58bb95 | ||
|
|
d95d4b7dd4 | ||
|
|
d261c3b494 | ||
|
|
8c94c97a28 | ||
|
|
46c316b795 | ||
|
|
7535d2e527 | ||
|
|
6cc1708abe | ||
|
|
e623672e81 | ||
|
|
006e0b7513 | ||
|
|
c5b5f3610e | ||
|
|
6571298061 | ||
|
|
9350a8b237 | ||
|
|
f4c263f380 | ||
|
|
9a920a10f3 | ||
|
|
2381096b06 | ||
|
|
0ddb43ac8a | ||
|
|
45705e0614 | ||
|
|
9557b9132b | ||
|
|
6d6e8845c1 | ||
|
|
5facfe55b8 | ||
|
|
35cd505e9e | ||
|
|
78d3e22052 | ||
|
|
7157b03a7e | ||
|
|
7663e4a4e3 | ||
|
|
ddc3d07bd4 | ||
|
|
f6d3fae25e | ||
|
|
952d0efde2 | ||
|
|
b7c3397471 | ||
|
|
cde4277e84 | ||
|
|
37b54f8e8a | ||
|
|
1304fd8a72 | ||
|
|
7b072893be | ||
|
|
d10d5be222 | ||
|
|
070afc64c8 | ||
|
|
20141518d9 | ||
|
|
d85a57118e | ||
|
|
d7d85870d2 | ||
|
|
d6d540e748 | ||
|
|
dbd3766a9d | ||
|
|
935a1ba903 | ||
|
|
0c20589a7f | ||
|
|
84325627aa | ||
|
|
9964210ac4 | ||
|
|
e62e82ba10 | ||
|
|
5a468c0877 | ||
|
|
71257ec00e | ||
|
|
4245ce33b5 | ||
|
|
317641c3bb | ||
|
|
2c65d96863 | ||
|
|
b1c179461d | ||
|
|
0bc02d6838 | ||
|
|
d54c40a3ed | ||
|
|
896f5a7e4c | ||
|
|
4be063be0f | ||
|
|
855ed636e7 | ||
|
|
b8097f53a6 | ||
|
|
54822af73a | ||
|
|
b55ee772b4 | ||
|
|
56f0e69e1f | ||
|
|
064f683781 | ||
|
|
54375713bc | ||
|
|
cb3dd19039 | ||
|
|
1c5f24a707 | ||
|
|
b7560be2b5 | ||
|
|
9d95c8c9c3 | ||
|
|
3582c00a61 | ||
|
|
8f1239674c | ||
|
|
01b35ae4eb | ||
|
|
82f8e06ff6 | ||
|
|
1a8ac80cdf | ||
|
|
b7c7fd9273 | ||
|
|
16641a5bb1 | ||
|
|
b7e5f0903b | ||
|
|
e40cf8043a | ||
|
|
32cb95c7fb | ||
|
|
f0303072f3 | ||
|
|
b2068531dd | ||
|
|
b29d7d2c8d | ||
|
|
b684929d1e | ||
|
|
47e53cbdaa | ||
|
|
086ac39095 | ||
|
|
bec4f6d5c7 | ||
|
|
cfa0b30905 | ||
|
|
5a4e88dc51 | ||
|
|
b627f003e3 | ||
|
|
07ba3dc197 | ||
|
|
5c9adeef93 | ||
|
|
7abfccc23e | ||
|
|
394d7a2ee6 | ||
|
|
987bb8dd35 | ||
|
|
ce314844d2 | ||
|
|
0871ace481 | ||
|
|
645263b0ac | ||
|
|
5478dd2733 | ||
|
|
9eb72ee0cc | ||
|
|
258178fe85 | ||
|
|
6978fafd28 | ||
|
|
c7ee362ae7 | ||
|
|
57e6cfd500 | ||
|
|
33bbc4f4b8 | ||
|
|
01a732274c | ||
|
|
a974d33c79 | ||
|
|
8549915afa | ||
|
|
59f940c19c | ||
|
|
ed1aecb251 | ||
|
|
7deb01267b | ||
|
|
4b9ab81848 | ||
|
|
6c8d801894 | ||
|
|
1a9de5f227 | ||
|
|
9b1eec8ade | ||
|
|
8e71cec208 | ||
|
|
0f4d71b8f8 | ||
|
|
6a6043daa3 | ||
|
|
1a697bd844 | ||
|
|
b23e100266 | ||
|
|
c9e8db38b6 | ||
|
|
0a7ac8b978 | ||
|
|
780b48db53 | ||
|
|
82d09ec909 | ||
|
|
b876639429 | ||
|
|
afe19e96fe | ||
|
|
c599fbb7a1 | ||
|
|
c63e9ef876 | ||
|
|
d6e828a2f3 | ||
|
|
2445f35a32 | ||
|
|
d2bcbc73d3 | ||
|
|
f46ed82d7e | ||
|
|
4222dfb69d | ||
|
|
1b3eb412b8 | ||
|
|
e9f6636df1 | ||
|
|
472e410205 | ||
|
|
581272ee51 | ||
|
|
e8e21e9d0b | ||
|
|
80d4742ded | ||
|
|
50939e0b1d | ||
|
|
5d5f34f65f | ||
|
|
d3b762eb97 | ||
|
|
0c60478953 | ||
|
|
3b23921a41 | ||
|
|
e3422eda58 | ||
|
|
37a4100220 | ||
|
|
8592c5cca3 | ||
|
|
014530f582 | ||
|
|
a994fbc865 | ||
|
|
0d14336ec5 | ||
|
|
c20c07c6d8 | ||
|
|
3a71b3f11e | ||
|
|
f2384694a1 | ||
|
|
ca46b8c805 | ||
|
|
189ff56224 | ||
|
|
ef6516e561 | ||
|
|
1d1fffdabc | ||
|
|
3fe32c0724 | ||
|
|
2e4e29ad73 | ||
|
|
9c4a08a55a | ||
|
|
5188633458 | ||
|
|
a8a24eba51 | ||
|
|
6493edcddf | ||
|
|
c186fbeafc | ||
|
|
d32c6db967 | ||
|
|
db6b3c4bcc | ||
|
|
50c5579c83 | ||
|
|
5a65fd9c6a | ||
|
|
be7fcbe485 | ||
|
|
99239e9160 | ||
|
|
662154123d | ||
|
|
cdaea7b0cd | ||
|
|
3cb374076a | ||
|
|
8d48616e0d | ||
|
|
3eeec14462 | ||
|
|
b5df612b4e | ||
|
|
fa778257b9 | ||
|
|
011b58dc74 | ||
|
|
b9b42efade | ||
|
|
20460a2be1 | ||
|
|
a649192c7f | ||
|
|
77461a3340 | ||
|
|
453688a555 | ||
|
|
504a8eb472 | ||
|
|
199e477349 | ||
|
|
3c8f600c57 | ||
|
|
263c6d75bf | ||
|
|
ffef3dba00 | ||
|
|
aa046b73fb | ||
|
|
cece3db0cb | ||
|
|
554f7dab72 | ||
|
|
b3c0c2bd3c | ||
|
|
72a557cc1a | ||
|
|
52d00a434e | ||
|
|
37c4cae5ed | ||
|
|
f41eea025e | ||
|
|
ac75059684 | ||
|
|
c71f947d06 | ||
|
|
f12b917930 | ||
|
|
6bca6bdc1a | ||
|
|
d260a725bc | ||
|
|
8edf0d5e1d | ||
|
|
5f2b298b81 | ||
|
|
7cda0017aa | ||
|
|
ab900017db | ||
|
|
8801926b10 | ||
|
|
ac71a8aea8 | ||
|
|
813e97df3b | ||
|
|
50e8727111 | ||
|
|
9e40614b5c | ||
|
|
b19b3f0fb6 | ||
|
|
7068954334 | ||
|
|
640ea60945 | ||
|
|
52518bdc47 | ||
|
|
fb43ff76af | ||
|
|
9c49b31609 | ||
|
|
d9b8c62cd4 | ||
|
|
2be3fc82ba | ||
|
|
862ca53d07 | ||
|
|
f2ef2b0b04 | ||
|
|
fec3a7f71a | ||
|
|
fa9126c4e6 | ||
|
|
a7b7674bb6 | ||
|
|
a706e010f4 | ||
|
|
441ec1bfb6 | ||
|
|
c4262e7df5 | ||
|
|
44be1f5d2a | ||
|
|
f2d0a5020a | ||
|
|
b3cd4948ad | ||
|
|
f0636ed083 | ||
|
|
7bd7967ab7 | ||
|
|
c1413ce58d | ||
|
|
ac116b958e | ||
|
|
53dbcbafb2 | ||
|
|
569708677f | ||
|
|
1a75416dbf | ||
|
|
ca4c9c3dab | ||
|
|
338d94daf9 | ||
|
|
24cbedb715 | ||
|
|
3be97841a3 | ||
|
|
158dcb06bd | ||
|
|
54ce0172b4 | ||
|
|
011026a8d1 | ||
|
|
da0928102b | ||
|
|
fe7f28a7a0 | ||
|
|
0aaa20f873 | ||
|
|
e5d66d63f5 | ||
|
|
86a30dc689 | ||
|
|
9fb1ce11fb | ||
|
|
9490c5ce1b | ||
|
|
42e0fcda29 | ||
|
|
e778df672a | ||
|
|
ae58302dfa | ||
|
|
9f24cf7e72 | ||
|
|
f1849cafaf | ||
|
|
f1eed44e14 | ||
|
|
ca88476934 | ||
|
|
bcfbbb481c | ||
|
|
9269e91c49 | ||
|
|
fd25ee0127 | ||
|
|
f43ae16a47 | ||
|
|
2b3c763982 | ||
|
|
3aace14492 | ||
|
|
9cdb8067f7 | ||
|
|
cee8f6727f | ||
|
|
7a2621f6ab | ||
|
|
03c18368cc | ||
|
|
5ea91c4d69 | ||
|
|
7c0a28eb59 | ||
|
|
936e0f80fa | ||
|
|
280ce85b74 | ||
|
|
cbe65f22ca | ||
|
|
c1b4f4edb1 | ||
|
|
6c834aecb4 | ||
|
|
bef865e196 | ||
|
|
8c25d57c26 | ||
|
|
6affe68ca3 | ||
|
|
012468785b | ||
|
|
9a48975ba9 | ||
|
|
63dcd6ef7a | ||
|
|
62aa91a34d | ||
|
|
323317b49e | ||
|
|
7344d62e81 | ||
|
|
0286f0cb93 | ||
|
|
b2121fde11 | ||
|
|
7549f0e924 | ||
|
|
f83ebdbe67 | ||
|
|
263e5b95cf | ||
|
|
6b7b279742 | ||
|
|
cf411ad798 | ||
|
|
d8899b0b5d | ||
|
|
07513c927f | ||
|
|
9104385883 | ||
|
|
d13afa1b37 | ||
|
|
0cbec2d0ec | ||
|
|
0a287e01ab | ||
|
|
51175437e9 | ||
|
|
7f5af09f52 | ||
|
|
930b1468a5 | ||
|
|
46aa8de4f6 | ||
|
|
d694febdbf | ||
|
|
f45f5b4b9f | ||
|
|
f8066d6dee | ||
|
|
df48e9f6d5 | ||
|
|
e96b472ede | ||
|
|
ec80fc9421 | ||
|
|
7e7dcd9917 | ||
|
|
6ac7e181c5 | ||
|
|
34d6449d07 | ||
|
|
46cb37876d | ||
|
|
927a142480 | ||
|
|
57c7e6af47 | ||
|
|
db845a6f24 | ||
|
|
12980fbe0c | ||
|
|
dda4904d9d | ||
|
|
454c2726a5 | ||
|
|
be16429317 | ||
|
|
370d0c562a | ||
|
|
3e9b1bafbe | ||
|
|
5affa70097 | ||
|
|
b6e5430702 | ||
|
|
97fa4a59b7 | ||
|
|
f685d8eb21 | ||
|
|
83ddf0fc4b | ||
|
|
a54a4321ea | ||
|
|
69ce01c794 | ||
|
|
ee759e245d | ||
|
|
6aa0782ce3 | ||
|
|
15bd827c2e | ||
|
|
4a200e59ac | ||
|
|
f769d03e0c | ||
|
|
468f419a58 | ||
|
|
3b8218e36e | ||
|
|
6dbdd03815 | ||
|
|
f43fcafc99 | ||
|
|
b15fbec69c | ||
|
|
a48cffd4ab | ||
|
|
3b3ef09d87 | ||
|
|
e01e9c9584 | ||
|
|
4c4160ae03 | ||
|
|
8c94980315 | ||
|
|
732927b0e2 | ||
|
|
4a9ecb33bd | ||
|
|
e82f475c9b | ||
|
|
12e1e422e3 | ||
|
|
dab73aa263 | ||
|
|
a6e6c07a3c | ||
|
|
40992a7f76 | ||
|
|
01babd1491 | ||
|
|
2191d1a16a | ||
|
|
9b2e511bcb | ||
|
|
7117951233 | ||
|
|
61ede1cc0d | ||
|
|
df17288c1e | ||
|
|
625e17ee1c | ||
|
|
91c7032aac | ||
|
|
238f6ab011 | ||
|
|
522c8bf3af | ||
|
|
a14bb3ead3 | ||
|
|
cb284447c8 | ||
|
|
41e4de35e9 | ||
|
|
a5c6252ff5 | ||
|
|
d3e42e342d | ||
|
|
5a4b305a68 | ||
|
|
0ceeb087e9 | ||
|
|
6f838091c3 | ||
|
|
cec71efc3e | ||
|
|
361998bd34 | ||
|
|
6ae71adcac | ||
|
|
8ec3368636 | ||
|
|
e746cd55dd | ||
|
|
1f019093fc | ||
|
|
aad6f8147e | ||
|
|
bc702d28ff | ||
|
|
96dd021ca4 | ||
|
|
c8e7edf11d | ||
|
|
d8c790cb7a | ||
|
|
29ddee4869 | ||
|
|
a2352f345b | ||
|
|
fa8a4853f7 | ||
|
|
a1f65a2f77 | ||
|
|
a554d3291f | ||
|
|
6f159aa2fc | ||
|
|
a78e07eda9 | ||
|
|
b7e96f9e54 | ||
|
|
c7ced80092 | ||
|
|
094bcbd81d | ||
|
|
19bd040453 | ||
|
|
a11ba0a89c | ||
|
|
2134e050cb | ||
|
|
e2ed603ab8 | ||
|
|
ff3781a431 | ||
|
|
55cd97ec68 | ||
|
|
2f0a4fc5c0 | ||
|
|
6dd61f96c0 | ||
|
|
fdae61380c | ||
|
|
09acb2b5f9 | ||
|
|
19c9fef8e2 | ||
|
|
c335682559 | ||
|
|
96806ee384 | ||
|
|
096655a3eb | ||
|
|
a9788c364d | ||
|
|
f593d14823 | ||
|
|
6801793547 | ||
|
|
8a9d843ad7 | ||
|
|
f52d631fa4 | ||
|
|
ca70d7b6b4 | ||
|
|
fe1d2f6af1 | ||
|
|
64b37ef518 | ||
|
|
654a0a671d | ||
|
|
dc1b1b4537 | ||
|
|
579e43c4cc | ||
|
|
f8d8288e0e | ||
|
|
ea7f89842a | ||
|
|
91a79e9dee | ||
|
|
a385ab0802 | ||
|
|
9175a220be | ||
|
|
0131fc9ebf | ||
|
|
d19a740825 | ||
|
|
c9eab73bd4 | ||
|
|
e42e20f776 | ||
|
|
d046e2a77f | ||
|
|
873dd76167 | ||
|
|
6824d6f4df | ||
|
|
74edc6fdd4 | ||
|
|
99926e67cd | ||
|
|
de712d9b43 | ||
|
|
01f68c50ed | ||
|
|
840774c034 | ||
|
|
da898b7efa | ||
|
|
d4f4e19056 | ||
|
|
94ba682b57 | ||
|
|
8cc6df1f70 | ||
|
|
7a0b462e7e | ||
|
|
24f431b290 | ||
|
|
0c47cd4d4d | ||
|
|
33f0105d29 | ||
|
|
bcbcc71bfc | ||
|
|
da39129bd2 | ||
|
|
973eb7b350 | ||
|
|
d229b775ab | ||
|
|
e91d90dbbc | ||
|
|
8c65fcacb3 | ||
|
|
e55b9fe922 | ||
|
|
8f9f2550ef | ||
|
|
673695068e | ||
|
|
07c123309f | ||
|
|
6a63be9107 | ||
|
|
978b2a9886 | ||
|
|
d1ce8a0586 | ||
|
|
dc61950311 | ||
|
|
0ba0845563 | ||
|
|
d70ba1dc23 | ||
|
|
9403478049 | ||
|
|
20f320327c | ||
|
|
9a5f85cc24 | ||
|
|
be32590ef0 | ||
|
|
e5c9b0e27f | ||
|
|
58527bb2ff | ||
|
|
4a7bbe6738 | ||
|
|
d616aaa4f8 | ||
|
|
376b29673b | ||
|
|
636e59b949 | ||
|
|
e3615f63e0 | ||
|
|
0b02a3c774 | ||
|
|
87ef131945 | ||
|
|
6370a936b8 | ||
|
|
e686527f87 | ||
|
|
c312317af0 | ||
|
|
b8af0ecf05 | ||
|
|
ab659fda5f | ||
|
|
27c87319c1 | ||
|
|
5d4cea2119 | ||
|
|
26cda84eab | ||
|
|
c4fc90e3f8 | ||
|
|
985040e1cc | ||
|
|
95fcf2f354 | ||
|
|
1422d935d0 | ||
|
|
5973e8f0dd | ||
|
|
934467d484 | ||
|
|
eedc314ca2 | ||
|
|
2556137b51 | ||
|
|
826493161a | ||
|
|
19716957e4 | ||
|
|
3d29357d1d | ||
|
|
9143cf4904 | ||
|
|
c7978f78a2 | ||
|
|
af26e27e23 | ||
|
|
7b7b7a36db | ||
|
|
8a1d2cf0f4 | ||
|
|
98f2576497 | ||
|
|
00fa7bec42 | ||
|
|
b03a25f4c7 | ||
|
|
cf8a3514bc | ||
|
|
b7320794bb | ||
|
|
d8113aa533 | ||
|
|
16a70bdeb7 | ||
|
|
0564ad4653 | ||
|
|
a57910e2fa | ||
|
|
4fa58c978b | ||
|
|
54cdbb3035 | ||
|
|
367eb45586 | ||
|
|
1124f8b513 | ||
|
|
233b78d258 | ||
|
|
5f96f33cf0 | ||
|
|
b511f1b52a | ||
|
|
6b3fd2f31a | ||
|
|
6501ab044c | ||
|
|
999116c536 | ||
|
|
f19d7f0b70 | ||
|
|
1e499c7867 | ||
|
|
b9d3deae56 | ||
|
|
0421f6ebcf | ||
|
|
a510ffb3c0 | ||
|
|
62f5b8291d | ||
|
|
f9945b3439 | ||
|
|
62ca4b7e13 | ||
|
|
6f59b56958 | ||
|
|
113086f97a | ||
|
|
b6d3120086 | ||
|
|
117f990055 | ||
|
|
337c11d580 | ||
|
|
ecb485d2f5 | ||
|
|
c5339116b5 | ||
|
|
b1877b4dc7 | ||
|
|
1c087f6bcd | ||
|
|
e40f340036 | ||
|
|
c096633f91 | ||
|
|
b074c2552a | ||
|
|
e2885f3a38 | ||
|
|
47ac4dd9fe | ||
|
|
fa7889cdd0 | ||
|
|
759379dfb1 | ||
|
|
87ed4f07cd | ||
|
|
07499380d1 | ||
|
|
3ae61fc1dd | ||
|
|
085a2b545f | ||
|
|
49cb6f74bc | ||
|
|
d4a93fe7e4 | ||
|
|
f4861aa74a | ||
|
|
1925f1f442 | ||
|
|
6355852d09 | ||
|
|
e1641c57fc | ||
|
|
7b920adbe9 | ||
|
|
05516750b5 | ||
|
|
b62a64519f | ||
|
|
e9e716964e | ||
|
|
a15f61bb85 | ||
|
|
de334ea116 | ||
|
|
dd952ac166 | ||
|
|
d2471b4301 | ||
|
|
04092c80e8 | ||
|
|
b7e0489a53 | ||
|
|
daf1128df7 | ||
|
|
36c436d4e0 | ||
|
|
edf783f155 | ||
|
|
dafe7ea3da | ||
|
|
cbe056c27e | ||
|
|
e78561b7b6 | ||
|
|
de791cfb35 | ||
|
|
6d1a2f3b57 | ||
|
|
e106cd02cf | ||
|
|
787ef30279 | ||
|
|
61519cc10c | ||
|
|
b666e6a924 | ||
|
|
61213159ee | ||
|
|
67934ff35f | ||
|
|
8c1833f117 | ||
|
|
c2f4c4dd32 | ||
|
|
c4ed61e831 | ||
|
|
5c5ca4e213 | ||
|
|
6edfa513f3 | ||
|
|
5170a12cb1 | ||
|
|
9a90f1f4ab | ||
|
|
886f285ab7 | ||
|
|
00bdd2805a | ||
|
|
d392bdb5ea | ||
|
|
145b4874cb | ||
|
|
869c11f1d8 | ||
|
|
8e7cdd2587 | ||
|
|
76fdf9d378 | ||
|
|
a7488456d8 | ||
|
|
5185e5f894 | ||
|
|
c19cc9b2d6 | ||
|
|
7968b7ce7e | ||
|
|
ee88fca8c0 | ||
|
|
9b3fbc9b84 | ||
|
|
db42dcfae2 | ||
|
|
cdd238d75e | ||
|
|
48a93c379f | ||
|
|
cc005a274d | ||
|
|
26bf1ab840 | ||
|
|
b4635eddf3 | ||
|
|
6b57921567 | ||
|
|
d500b0f204 | ||
|
|
9985ae2c38 | ||
|
|
bce3269e44 | ||
|
|
ae037bdac0 | ||
|
|
6f6378aaf2 | ||
|
|
187f072f43 | ||
|
|
3ab2376517 | ||
|
|
26502114d4 | ||
|
|
5db479c25d | ||
|
|
d0b555687f | ||
|
|
041e259c11 | ||
|
|
d59f4d2f6f | ||
|
|
898a18db21 | ||
|
|
a73fe62b7a | ||
|
|
350f6cfea1 | ||
|
|
2936f80013 | ||
|
|
dec9f6d254 | ||
|
|
bd6676d3b6 | ||
|
|
d9309d33af | ||
|
|
bb9802e754 | ||
|
|
eec804dd7c | ||
|
|
ff6d3a0768 | ||
|
|
94d37bbe4c | ||
|
|
36136a4c29 | ||
|
|
8b273e6939 | ||
|
|
2340fa1c91 | ||
|
|
70051da2ab | ||
|
|
5659bf3283 | ||
|
|
8de8ac10d4 | ||
|
|
46c93f1b63 | ||
|
|
e3a9d0d14f | ||
|
|
0a4b403f6a | ||
|
|
b94b4f5353 | ||
|
|
d84576e160 | ||
|
|
a0903fbf9b | ||
|
|
4b29b8a9c6 | ||
|
|
c994c67dd9 | ||
|
|
0b09f0bafd | ||
|
|
54bb0ee189 | ||
|
|
68b7f88523 | ||
|
|
a9eb1de4b4 | ||
|
|
28bcd88fb0 | ||
|
|
bee4b0ff06 | ||
|
|
559b3c1392 | ||
|
|
d3c9f70bec | ||
|
|
4ba7aef441 | ||
|
|
2f07ed9976 | ||
|
|
ff74caf50d | ||
|
|
3d941f2fdf | ||
|
|
93aab95f7b | ||
|
|
079702282c | ||
|
|
db8e449a8a | ||
|
|
a2480bae51 | ||
|
|
cd501f9dcc | ||
|
|
354f3e03a2 | ||
|
|
b07ca8bb24 | ||
|
|
22d777c9f9 | ||
|
|
adfeffec76 | ||
|
|
4a1bdad759 | ||
|
|
00ef60ebaf | ||
|
|
0ad9f70645 | ||
|
|
4da5d557a5 | ||
|
|
a1dc85ee57 | ||
|
|
554b4a8224 | ||
|
|
3f659a15da | ||
|
|
dd5b295219 | ||
|
|
9d0200252b | ||
|
|
d67faff92a | ||
|
|
c50c134e25 | ||
|
|
bc1048062d | ||
|
|
74ea558bfc | ||
|
|
c6f93701f5 | ||
|
|
8a4772ca39 | ||
|
|
66c80d8b78 | ||
|
|
0cb38cfdcb | ||
|
|
b0605e4cd1 | ||
|
|
82a9ee62d7 | ||
|
|
8b2b0f6abc | ||
|
|
0f9240207d | ||
|
|
c669c3fba7 | ||
|
|
8860726c68 | ||
|
|
f64724f54f | ||
|
|
fd222b58b5 | ||
|
|
83ba481e3d | ||
|
|
7d5bbc341f | ||
|
|
519e44d30e | ||
|
|
3bb2c158fd | ||
|
|
5ada5f0c0c | ||
|
|
ea1fcc6a5f | ||
|
|
19c14b076b | ||
|
|
c015379c51 | ||
|
|
2f88498ad5 | ||
|
|
5e6e65c8da | ||
|
|
157055c03b | ||
|
|
676aabc3ed | ||
|
|
5c33e4008d | ||
|
|
772e446192 | ||
|
|
66be7797cf | ||
|
|
3261c53c08 | ||
|
|
5bab94730f | ||
|
|
801aeb8462 | ||
|
|
cbe5abcf27 | ||
|
|
72dbd43be6 | ||
|
|
7145130835 | ||
|
|
b34289cc44 | ||
|
|
fe2b1c0d43 | ||
|
|
4de6f1c255 | ||
|
|
8baff9bade | ||
|
|
f8f7e7e4c1 | ||
|
|
7523505af1 | ||
|
|
d94b119e9d | ||
|
|
3cab8511e4 | ||
|
|
7d3f5375a8 | ||
|
|
b2c4ed638b | ||
|
|
2804bfa8b7 | ||
|
|
64ac67026a | ||
|
|
5a74ce2e59 | ||
|
|
f4a56389cd | ||
|
|
52b64beefc | ||
|
|
ec1234e8d8 | ||
|
|
b7695d2fd6 | ||
|
|
4c6dc45d20 | ||
|
|
5cb048094e | ||
|
|
e3cccd619b | ||
|
|
88f0b91056 | ||
|
|
78ddb9feba | ||
|
|
f68ff2a455 | ||
|
|
04e2bdd6ec | ||
|
|
84fa6be370 | ||
|
|
af2f744186 | ||
|
|
5f4b719444 | ||
|
|
9616851e52 | ||
|
|
4645fda126 | ||
|
|
057a8f6f1b | ||
|
|
e6917d1933 | ||
|
|
6535995753 | ||
|
|
77ed5ba7ed | ||
|
|
bc86b7804c | ||
|
|
d5f4ae53c9 | ||
|
|
b898e016f3 | ||
|
|
712f0ae2a7 | ||
|
|
c553107d23 | ||
|
|
b127436cc4 | ||
|
|
b61ac11a76 | ||
|
|
b22d7f1140 | ||
|
|
925aaf18a1 | ||
|
|
797ed47791 | ||
|
|
4128a6e2f1 | ||
|
|
69f34be4b8 | ||
|
|
1c3da7e2e6 | ||
|
|
5c7644adc0 | ||
|
|
f974010aa3 | ||
|
|
d4d18f741f | ||
|
|
d97b54b9be | ||
|
|
a7b94fbe1a | ||
|
|
2ab9de91fe | ||
|
|
fd6bf6d667 | ||
|
|
38e81f053f | ||
|
|
c6f1b456f5 | ||
|
|
8484978ef0 | ||
|
|
c4919b9a6b | ||
|
|
02e4db4c82 | ||
|
|
5200766617 | ||
|
|
8d9badb588 | ||
|
|
fc534a1067 | ||
|
|
f68ef19868 | ||
|
|
0a2e40502f | ||
|
|
e843a2f61e | ||
|
|
d95a633f16 | ||
|
|
6f41aad2ef | ||
|
|
e76cbd9c3f | ||
|
|
0c8b299996 | ||
|
|
86420d2535 | ||
|
|
9e4dcdaf91 | ||
|
|
45e7bf274a | ||
|
|
62cf413d54 | ||
|
|
3e0a270891 | ||
|
|
22d79eef26 | ||
|
|
31d806afce | ||
|
|
1e9ca83120 | ||
|
|
3d9a33bb69 | ||
|
|
ea989ed691 | ||
|
|
ee2c5969fa | ||
|
|
8e151ee6ef | ||
|
|
dc904b27a2 | ||
|
|
4de539a1ff | ||
|
|
913de3158e | ||
|
|
049ca653ed | ||
|
|
246d137c2c | ||
|
|
99b1a7a49b | ||
|
|
9fa068f574 | ||
|
|
eb5f964bcb | ||
|
|
91337d7e57 | ||
|
|
cb555d5297 | ||
|
|
50dd3cb841 | ||
|
|
b7750cbf91 | ||
|
|
dde47269de | ||
|
|
bc3f2a12c0 | ||
|
|
bdb112b23c | ||
|
|
214ccd14ac | ||
|
|
e5f3820285 | ||
|
|
f75b4cfa93 | ||
|
|
f5d651dda1 | ||
|
|
a90254e778 | ||
|
|
a6bcfcfba4 | ||
|
|
78ae1d09f7 | ||
|
|
1132da8563 | ||
|
|
199b9ceb6d | ||
|
|
8e41d22f8d | ||
|
|
57bd61697c | ||
|
|
3e8b8dc6ef | ||
|
|
b4bc830cee | ||
|
|
2e43f615f6 | ||
|
|
5f2d1d2b4e | ||
|
|
4ee305fa21 | ||
|
|
4b3e16dbd0 | ||
|
|
a7c836e42c | ||
|
|
886c057d0f | ||
|
|
038704786d | ||
|
|
c61076830f | ||
|
|
02fccfbdc7 | ||
|
|
b250cedf81 | ||
|
|
7243e330fc | ||
|
|
17a7fb2a6e | ||
|
|
d3d8ec3b20 | ||
|
|
b6074df4bf | ||
|
|
f3cc9469a7 | ||
|
|
a9d2b8a55c | ||
|
|
73e235d9f8 | ||
|
|
17ec85e39e | ||
|
|
723c64e481 | ||
|
|
faa60be6fb | ||
|
|
e95f9815d8 | ||
|
|
9b62d8a01a | ||
|
|
1c206c2c27 | ||
|
|
782797ca11 | ||
|
|
b34ee5157e | ||
|
|
ae37c43cd4 | ||
|
|
272b1d2194 | ||
|
|
260e2dab16 | ||
|
|
7c78390e2b | ||
|
|
f5a4eed27c | ||
|
|
b0768f18ca | ||
|
|
4d1e532069 | ||
|
|
5beb7f52c2 | ||
|
|
eaaf9b7af1 | ||
|
|
3c9687decf | ||
|
|
a5e269a3f9 | ||
|
|
30d84b1202 | ||
|
|
0aa1079cd0 | ||
|
|
1091255d2c | ||
|
|
56e63d762b | ||
|
|
f3c8b8f395 | ||
|
|
47be9a2df4 | ||
|
|
aee1629c25 | ||
|
|
8587eea54c | ||
|
|
d7ed109f9d | ||
|
|
84dfe8fccc | ||
|
|
6904dc5009 | ||
|
|
b89227d365 | ||
|
|
6c01630090 | ||
|
|
10282e4cb1 | ||
|
|
8518b354a7 | ||
|
|
00a8b80ef7 | ||
|
|
1a0a56c1f2 | ||
|
|
055267ee9c | ||
|
|
6e9040e6ba | ||
|
|
a32776e2af | ||
|
|
1d6d6eb716 | ||
|
|
881a4a0dbb | ||
|
|
003cf96910 | ||
|
|
3cb87af315 | ||
|
|
edee1a1067 | ||
|
|
e32d8774eb | ||
|
|
49d5608fa6 | ||
|
|
56bfdb64d4 | ||
|
|
9513c231c9 | ||
|
|
dd92161da5 | ||
|
|
55571a1ae8 | ||
|
|
c112990d8f | ||
|
|
23dba6aebd | ||
|
|
f669b8ed1d | ||
|
|
9a22cbe44b | ||
|
|
883ddba934 | ||
|
|
8f7295b8a2 | ||
|
|
0ba97f6941 | ||
|
|
d979c077a0 | ||
|
|
4e18a78c02 | ||
|
|
1d6e8abcca | ||
|
|
82a65be446 | ||
|
|
0778a823d1 | ||
|
|
eca36458b3 |
@@ -1,4 +1,4 @@
|
||||
FROM node:10
|
||||
FROM node:14
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig"
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
}
|
||||
|
||||
+27
-6
@@ -9,17 +9,25 @@
|
||||
"project": [
|
||||
"src/browser/tsconfig.json",
|
||||
"src/common/tsconfig.json",
|
||||
"src/headless/tsconfig.json",
|
||||
"test/api/tsconfig.json",
|
||||
"test/benchmark/tsconfig.json",
|
||||
"addons/xterm-addon-attach/src/tsconfig.json",
|
||||
"addons/xterm-addon-attach/test/tsconfig.json",
|
||||
"addons/xterm-addon-fit/src/tsconfig.json",
|
||||
"addons/xterm-addon-fit/test/tsconfig.json",
|
||||
"addons/xterm-addon-ligatures/src/tsconfig.json",
|
||||
"addons/xterm-addon-search/src/tsconfig.json",
|
||||
"addons/xterm-addon-unicode11/src/tsconfig.json",
|
||||
"addons/xterm-addon-web-links/src/tsconfig.json",
|
||||
"addons/xterm-addon-webgl/src/tsconfig.json",
|
||||
"addons/xterm-addon-search/test/tsconfig.json",
|
||||
"addons/xterm-addon-serialize/src/tsconfig.json",
|
||||
"addons/xterm-addon-serialize/test/tsconfig.json",
|
||||
"addons/xterm-addon-serialize/benchmark/tsconfig.json",
|
||||
"addons/xterm-addon-ligatures/src/tsconfig.json"
|
||||
"addons/xterm-addon-unicode11/src/tsconfig.json",
|
||||
"addons/xterm-addon-unicode11/test/tsconfig.json",
|
||||
"addons/xterm-addon-web-links/src/tsconfig.json",
|
||||
"addons/xterm-addon-web-links/test/tsconfig.json",
|
||||
"addons/xterm-addon-webgl/src/tsconfig.json",
|
||||
"addons/xterm-addon-webgl/test/tsconfig.json"
|
||||
],
|
||||
"sourceType": "module"
|
||||
},
|
||||
@@ -36,10 +44,11 @@
|
||||
"@typescript-eslint/array-type": [
|
||||
"warn",
|
||||
{
|
||||
"default": "array-simple",
|
||||
"default": "array",
|
||||
"readonly": "generic"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/consistent-type-assertions": "warn",
|
||||
"@typescript-eslint/consistent-type-definitions": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
@@ -144,6 +153,10 @@
|
||||
"warn",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "warn",
|
||||
"spaced-comment": [
|
||||
"warn",
|
||||
@@ -153,5 +166,13 @@
|
||||
"exceptions": ["-"]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts"],
|
||||
"rules": {
|
||||
"object-curly-spacing": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
NODE_PATH=./out
|
||||
@@ -0,0 +1,11 @@
|
||||
require:
|
||||
- source-map-support/register
|
||||
spec:
|
||||
- out/**/*.test.js
|
||||
- addons/**/out/*.test.js
|
||||
watch-files:
|
||||
- out/**/*.js
|
||||
- addons/**/out/*.js
|
||||
reporter: spec
|
||||
color: true
|
||||
check-leaks: true
|
||||
+8
-22
@@ -2,10 +2,6 @@
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - entries to be included must be negated with "!"
|
||||
!*.js
|
||||
!*.json
|
||||
|
||||
# Whitelist - css/
|
||||
!css/**/*.css
|
||||
|
||||
@@ -32,25 +28,15 @@
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
# Blacklist - (normal behavior) these will override any whitelist
|
||||
*.d.ts.map
|
||||
*.test.ts
|
||||
*.test.d.ts
|
||||
*.test.js
|
||||
*.test.js.map
|
||||
lib/test/
|
||||
webpack.config.js
|
||||
|
||||
docs/
|
||||
/.idea/
|
||||
.vscode/
|
||||
bin/
|
||||
build/
|
||||
fixtures/
|
||||
demo/
|
||||
.devcontainer/
|
||||
out/
|
||||
addons/
|
||||
headless/
|
||||
typings/xterm-headless.d.ts
|
||||
|
||||
Vendored
+2
-1
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.quoteStyle": "single"
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"mochaExplorer.envPath": ".mocha.env"
|
||||
}
|
||||
|
||||
Vendored
+73
-49
@@ -1,51 +1,75 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
]
|
||||
"version": "2.0.0",
|
||||
"presentation": {
|
||||
"echo": false,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "dedicated",
|
||||
"showReuseMessage": true
|
||||
},
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "test",
|
||||
"group":{
|
||||
"kind": "test",
|
||||
"isDefault": true
|
||||
},
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "watch",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "start",
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"dependsOn": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": "start",
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"fileLocation": [
|
||||
"relative",
|
||||
"${workspaceFolder}"
|
||||
],
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^([^\\\\s].*)\\\\((\\\\d+,\\\\d+)\\\\):\\\\s*(.*)$",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"beginsPattern": "assets by",
|
||||
"endsPattern": "webpack \\d+\\.\\d+\\.\\d+ compiled successfully"
|
||||
}
|
||||
},
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,11 +6,11 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
|
||||
|
||||
## Features
|
||||
|
||||
- **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.
|
||||
- **Terminal apps just work**: Xterm.js works with most terminal apps such as `bash`, `vim`, and `tmux`, including support for curses-based apps and mouse events.
|
||||
- **Performant**: Xterm.js is *really* fast, it even includes a GPU-accelerated renderer.
|
||||
- **Rich unicode support**: Supports CJK, emojis and IMEs.
|
||||
- **Rich Unicode support**: Supports CJK, emojis, and IMEs.
|
||||
- **Self-contained**: Requires zero dependencies to work.
|
||||
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on
|
||||
- **Accessible**: Screen reader and minimum contrast ratio support can be turned on.
|
||||
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||
|
||||
## What xterm.js is not
|
||||
@@ -20,13 +20,13 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
|
||||
|
||||
## Getting Started
|
||||
|
||||
First you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/) so you need that installed and then add xterm.js as a dependency by running:
|
||||
First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running:
|
||||
|
||||
```
|
||||
```bash
|
||||
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. Finally instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`.
|
||||
|
||||
```html
|
||||
<!doctype html>
|
||||
@@ -58,7 +58,7 @@ import { Terminal } from 'xterm';
|
||||
|
||||
⚠️ *This section describes the new addon format introduced in v3.14.0, see [here](https://github.com/xtermjs/xterm.js/blob/3.14.2/README.md#addons) for the instructions on the old format*
|
||||
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon you first need to install it in your project:
|
||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
|
||||
|
||||
```bash
|
||||
npm i -S xterm-addon-web-links
|
||||
@@ -76,7 +76,7 @@ const terminal = new Terminal();
|
||||
terminal.loadAddon(new WebLinksAddon());
|
||||
```
|
||||
|
||||
The xterm.js team maintains the following addons but they can be built by anyone:
|
||||
The xterm.js team maintains the following addons, but anyone can build them:
|
||||
|
||||
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
|
||||
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
|
||||
@@ -85,23 +85,27 @@ The xterm.js team maintains the following addons but they can be built by anyone
|
||||
|
||||
## Browser Support
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
|
||||
|
||||
We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
||||
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow 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.
|
||||
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.
|
||||
|
||||
### Node.js Support
|
||||
|
||||
We also publish [`xterm-headless`](https://www.npmjs.com/package/xterm-headless) which is a stripped down version of xterm.js that runs in Node.js. An example use case for this is to keep track of a terminal's state where the process is running and using the serialize addon so it can get all state restored upon reconnection.
|
||||
|
||||
## 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.
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
|
||||
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
||||
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
||||
@@ -113,10 +117,10 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Next Tech**](https://next.tech "Next Tech"): Online platform for interactive coding and web development courses. Live container-backed terminal uses xterm.js.
|
||||
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||
- [**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.
|
||||
- [**Gravitational Teleport**](https://github.com/gravitational/teleport): Gravitational Teleport is a modern SSH server for remotely accessing clusters of Linux servers via SSH or HTTPS.
|
||||
- [**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 scalable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
|
||||
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
@@ -143,7 +147,6 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**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.
|
||||
- [**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.
|
||||
@@ -163,14 +166,35 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**CoCalc**](https://cocalc.com/): Lots of free software pre-installed, to chat, collaborate, develop, program, publish, research, share, teach, in C++, HTML, Julia, Jupyter, LaTeX, Markdown, Python, R, SageMath, Scala, ...
|
||||
- [**Dank Domain**](https://www.DDgame.us/): Open source multiuser medieval game supporting old & new terminal emulation.
|
||||
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
|
||||
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
|
||||
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
|
||||
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
|
||||
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
|
||||
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
|
||||
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages supported, with results displayed by xterm.js.
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
|
||||
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
|
||||
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
|
||||
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
|
||||
- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH.
|
||||
- [**gifcast**](https://dstein64.github.io/gifcast/): Converts an asciinema cast to an animated GIF.
|
||||
- [**WizardWebssh**](https://gitlab.com/mikeramsey/wizardwebssh): A terminal with Pyqt5 Widget for embedding, which can be used as an ssh client to connect to your ssh servers. It is written in Python, based on tornado, paramiko, and xterm.js.
|
||||
- [**Wizard Assistant**](https://wizardassistant.com/): Wizard Assistant comes with advanced automation tools, preloaded common and special time-saving commands, and a built-in SSH terminal. Now you can remotely administer, troubleshoot, and analyze any system with ease.
|
||||
- [**ucli**](https://github.com/tsadarsh/ucli): Command Line for everyone :family_man_woman_girl_boy: at [www.ucli.tech](https://www.ucli.tech).
|
||||
- [**Tess**](https://github.com/SquitchYT/Tess/): Simple Terminal Fully Customizable for Everyone. Discover more at [tessapp.dev](https://tessapp.dev)
|
||||
- [**HashiCorp Nomad**](https://www.nomadproject.io/): A container orchestrator with the ability to connect to remote tasks via a web interface using websockets and xterm.js.
|
||||
- [**TermPair**](https://github.com/cs01/termpair): View and control terminals from your browser with end-to-end encryption
|
||||
- [**gdbgui**](https://github.com/cs01/gdbgui): Browser-based frontend to gdb (gnu debugger)
|
||||
- [**goormIDE**](https://ide.goorm.io/): Run almost every programming languages with real-time collaboration, live pair programming, and built-in messenger.
|
||||
- [**FleetDeck**](https://fleetdeck.io): Remote desktop & virtual terminal
|
||||
- [**OpenSumi**](https://github.com/opensumi/core): A framework helps you quickly build Cloud or Desktop IDE products.
|
||||
- [**KubeSail**](https://kubesail.com): The Self-Hosting Company - uses xterm to allow users to exec into kubernetes pods and build github apps
|
||||
- [**WiTTY**](https://github.com/syssecfsu/witty): Web-based interactive terminal emulator that allows users to easily record, share, and replay console sessions.
|
||||
- [**libv86 Terminal Forwarding**](https://github.com/hello-smile6/libv86-terminal-forwarding): Peer-to-peer SSH for the web, using WebRTC via [Bugout](https://github.com/chr15m/bugout) for data transfer and [v86](https://github.com/copy/v86) for web-based virtualization.
|
||||
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
|
||||
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
|
||||
- [**CloudTTY**](https://github.com/cloudtty/cloudtty): A Friendly Kubernetes CloudShell (Web Terminal).
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
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 on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
@@ -180,21 +204,21 @@ All current and past releases are available on this repo's [Releases page](https
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master, install the latest beta build with:
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
|
||||
```
|
||||
```bash
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable but some bugs may slip in, we recommend using the beta build primarily to test out new features and for verifying bug fixes.
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
|
||||
## 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.
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
|
||||
## 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 implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -9,8 +9,13 @@
|
||||
"types": "typings/xterm-addon-attach.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -20,7 +20,7 @@ export class AttachAddon implements ITerminalAddon {
|
||||
this._socket = socket;
|
||||
// always set binary type to arraybuffer, we do not handle blobs
|
||||
this._socket.binaryType = 'arraybuffer';
|
||||
this._bidirectional = (options && options.bidirectional === false) ? false : true;
|
||||
this._bidirectional = !(options && options.bidirectional === false);
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -41,7 +41,9 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
for (const d of this._disposables) {
|
||||
d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+4
-7
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import WebSocket = require('ws');
|
||||
import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -16,10 +16,7 @@ const height = 600;
|
||||
|
||||
describe('AttachAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-fit",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,8 +9,13 @@
|
||||
"types": "typings/xterm-addon-fit.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -33,7 +33,7 @@ export class FitAddon implements ITerminalAddon {
|
||||
|
||||
public fit(): void {
|
||||
const dims = this.proposeDimensions();
|
||||
if (!dims || !this._terminal) {
|
||||
if (!dims || !this._terminal || isNaN(dims.cols) || isNaN(dims.rows)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@ export class FitAddon implements ITerminalAddon {
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (this._terminal as any)._core;
|
||||
|
||||
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const scrollbarWidth = this._terminal.options.scrollback === 0 ?
|
||||
0 : core.viewport.scrollBarWidth;
|
||||
|
||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||
const parentElementWidth = Math.max(0, parseInt(parentElementStyle.getPropertyValue('width')));
|
||||
@@ -72,7 +79,7 @@ export class FitAddon implements ITerminalAddon {
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - core.viewport.scrollBarWidth;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+23
-9
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -16,13 +16,14 @@ const height = 768;
|
||||
|
||||
describe('FitAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
});
|
||||
|
||||
beforeEach(async function(): Promise<any> {
|
||||
await page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
|
||||
await openTerminal(page);
|
||||
});
|
||||
|
||||
@@ -30,6 +31,10 @@ describe('FitAddon', () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
afterEach(async function(): Promise<any> {
|
||||
await page.evaluate(`window.term.dispose()`);
|
||||
});
|
||||
|
||||
it('no terminal', async function(): Promise<any> {
|
||||
await page.evaluate(`window.fit = new FitAddon();`);
|
||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||
@@ -37,7 +42,7 @@ describe('FitAddon', () => {
|
||||
|
||||
describe('proposeDimensions', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
@@ -63,11 +68,20 @@ describe('FitAddon', () => {
|
||||
rows: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('hidden', async function(): Promise<any> {
|
||||
await page.evaluate(`window.term.dispose()`);
|
||||
await page.evaluate(`document.querySelector('#terminal-container').style.display='none'`);
|
||||
await page.evaluate(`window.term = new Terminal()`);
|
||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||
await loadFit();
|
||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fit', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -2,21 +2,13 @@
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - entries to be included must be negated with "!"
|
||||
!*.js
|
||||
!*.json
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
# Whitelist - out/
|
||||
!out/**/*.d.ts
|
||||
|
||||
!out/**/*.js
|
||||
!out/**/*.js.map
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
@@ -25,24 +17,13 @@
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/**/*.d.ts
|
||||
|
||||
# Blacklist - (normal behavior) these will override any whitelist
|
||||
*.test.ts
|
||||
*.test.d.ts
|
||||
*.test.js
|
||||
*.test.js.map
|
||||
|
||||
docs/
|
||||
/.idea/
|
||||
.vscode/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
fonts/
|
||||
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -51,3 +51,4 @@ This package makes use of the following fonts for testing:
|
||||
[Fira Code License]: https://github.com/tonsky/FiraCode/blob/master/LICENSE
|
||||
[Iosevka]: https://github.com/be5invis/Iosevka
|
||||
[Iosevka License]: https://github.com/be5invis/Iosevka/blob/master/LICENSE.md
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-ligatures",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.3",
|
||||
"description": "Add support for programming ligatures to xterm.js",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
@@ -25,18 +25,18 @@
|
||||
"keywords": [
|
||||
"font",
|
||||
"ligature",
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js",
|
||||
"terminal"
|
||||
"xterm.js"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.0.4",
|
||||
"font-ligatures": "^1.3.3"
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/sinon": "^5.0.1",
|
||||
"axios": "^0.18.0",
|
||||
"axios": "^0.21.2",
|
||||
"mkdirp": "0.5.5",
|
||||
"sinon": "6.3.5",
|
||||
"yauzl": "^2.10.0"
|
||||
|
||||
@@ -3,12 +3,28 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as fontFinder from 'font-finder';
|
||||
import * as fontLigatures from 'font-ligatures';
|
||||
import { FontList } from 'font-finder';
|
||||
import { Font, loadBuffer, loadFile } from 'font-ligatures';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
let fontsPromise: Promise<fontFinder.FontList> | undefined = undefined;
|
||||
interface IFontMetadata {
|
||||
family: string;
|
||||
fullName: string;
|
||||
postscriptName: string;
|
||||
blob: () => Promise<Blob>;
|
||||
}
|
||||
|
||||
interface IFontAccessNavigator {
|
||||
fonts: {
|
||||
query: () => Promise<IFontMetadata[]>;
|
||||
};
|
||||
permissions: {
|
||||
request?: (permission: { name: string }) => Promise<{state: string}>;
|
||||
};
|
||||
}
|
||||
|
||||
let fontsPromise: Promise<FontList | Record<string, IFontMetadata[]>> | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Loads the font ligature wrapper for the specified font family if it could be
|
||||
@@ -16,9 +32,50 @@ let fontsPromise: Promise<fontFinder.FontList> | undefined = undefined;
|
||||
* @param fontFamily The CSS font family definition to resolve
|
||||
* @param cacheSize The size of the ligature cache to maintain if the font is resolved
|
||||
*/
|
||||
export default async function load(fontFamily: string, cacheSize: number): Promise<fontLigatures.Font | undefined> {
|
||||
export default async function load(fontFamily: string, cacheSize: number): Promise<Font | undefined> {
|
||||
if (!fontsPromise) {
|
||||
fontsPromise = fontFinder.list();
|
||||
// Web environment that supports font access API
|
||||
if (typeof navigator !== 'undefined' && 'fonts' in navigator) {
|
||||
try {
|
||||
const status = await (navigator as unknown as IFontAccessNavigator).permissions.request?.({
|
||||
name: 'local-fonts'
|
||||
});
|
||||
if (status && status.state !== 'granted') {
|
||||
throw new Error('Permission to access local fonts not granted.');
|
||||
}
|
||||
} catch (err: any) {
|
||||
// A `TypeError` indicates the 'local-fonts'
|
||||
// permission is not yet implemented, so
|
||||
// only `throw` if this is _not_ the problem.
|
||||
if (err.name !== 'TypeError') {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const fonts: Record<string, IFontMetadata[]> = {};
|
||||
try {
|
||||
const fontsIterator = await (navigator as unknown as IFontAccessNavigator).fonts.query();
|
||||
for (const metadata of fontsIterator) {
|
||||
if (!fonts.hasOwnProperty(metadata.family)) {
|
||||
fonts[metadata.family] = [];
|
||||
}
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
// Node environment or no font access API
|
||||
else {
|
||||
try {
|
||||
fontsPromise = (await import('font-finder')).list();
|
||||
} catch (err) {
|
||||
// No-op
|
||||
}
|
||||
}
|
||||
if (!fontsPromise) {
|
||||
fontsPromise = Promise.resolve({});
|
||||
}
|
||||
}
|
||||
|
||||
const fonts = await fontsPromise;
|
||||
@@ -31,7 +88,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
}
|
||||
|
||||
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
|
||||
return await fontLigatures.loadFile(fonts[family][0].path, { cacheSize });
|
||||
const font = fonts[family][0];
|
||||
if ('blob' in font) {
|
||||
return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize });
|
||||
}
|
||||
return await loadFile(font.path, { cacheSize });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('handles quoted font names', done => {
|
||||
term.setOption('fontFamily', '"Fira Code", monospace');
|
||||
term.options.fontFamily = '"Fira Code", monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||
@@ -87,7 +87,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('falls back to later fonts if earlier ones are not present', done => {
|
||||
term.setOption('fontFamily', 'notinstalled, Fira Code, monospace');
|
||||
term.options.fontFamily = 'notinstalled, Fira Code, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||
@@ -98,17 +98,17 @@ describe('xterm-addon-ligatures', () => {
|
||||
it('uses the current font value', done => {
|
||||
// The first three calls are all synchronous so that we don't allow time for
|
||||
// any fonts to load while we're switching things around
|
||||
term.setOption('fontFamily', 'Fira Code');
|
||||
term.options.fontFamily = 'Fira Code';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.setOption('fontFamily', 'notinstalled');
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.setOption('fontFamily', 'Iosevka');
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4]]);
|
||||
|
||||
// And switch it back to Fira Code for good measure
|
||||
term.setOption('fontFamily', 'Fira Code');
|
||||
term.options.fontFamily = 'Fira Code';
|
||||
|
||||
// At this point, we haven't loaded the new font, so the result reverts
|
||||
// back to empty until that happens
|
||||
@@ -124,7 +124,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
it('allows multiple terminal instances that use different fonts', done => {
|
||||
const onRefresh2 = sinon.stub();
|
||||
const term2 = new MockTerminal(onRefresh2);
|
||||
term2.setOption('fontFamily', 'Iosevka');
|
||||
term2.options.fontFamily = 'Iosevka';
|
||||
ligatureSupport.enableLigatures(term2 as any);
|
||||
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
@@ -140,7 +140,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('fails if it finds but cannot load the font', async () => {
|
||||
term.setOption('fontFamily', 'Nonexistant Font, monospace');
|
||||
term.options.fontFamily = 'Nonexistant Font, monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -148,7 +148,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if the font is not present on the system', async () => {
|
||||
term.setOption('fontFamily', 'notinstalled');
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -156,7 +156,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if no specific font is specified', async () => {
|
||||
term.setOption('fontFamily', 'monospace');
|
||||
term.options.fontFamily = 'monospace';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -164,7 +164,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('returns nothing if no fonts are provided', async () => {
|
||||
term.setOption('fontFamily', '');
|
||||
term.options.fontFamily = '';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -172,7 +172,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
});
|
||||
|
||||
it('fails when given malformed inputs', async () => {
|
||||
term.setOption('fontFamily', {} as any);
|
||||
term.options.fontFamily = {} as any;
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -181,7 +181,7 @@ describe('xterm-addon-ligatures', () => {
|
||||
|
||||
it('ensures no empty errors are thrown', async () => {
|
||||
sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; });
|
||||
term.setOption('fontFamily', 'Iosevka');
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
await delay(500);
|
||||
assert.isTrue(onRefresh.notCalled);
|
||||
@@ -209,11 +209,11 @@ class MockTerminal {
|
||||
public deregisterCharacterJoiner(id: number): void {
|
||||
this.joiner = undefined;
|
||||
}
|
||||
public setOption(name: string, value: string | number): void {
|
||||
this._options[name] = value;
|
||||
}
|
||||
public getOption(name: string): string | number {
|
||||
return this._options[name];
|
||||
public get options(): { [name: string]: string | number } { return this._options; }
|
||||
public set options(options: { [name: string]: string | number }) {
|
||||
for (const key in this._options) {
|
||||
this._options[key] = options[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export function enableLigatures(term: Terminal): void {
|
||||
|
||||
term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
const termFont = term.getOption('fontFamily');
|
||||
const termFont = term.options.fontFamily;
|
||||
if (
|
||||
termFont &&
|
||||
(loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
|
||||
@@ -48,20 +48,20 @@ export function enableLigatures(term: Terminal): void {
|
||||
.then(f => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.LOADED;
|
||||
font = f;
|
||||
|
||||
// Only refresh things if we actually found a font
|
||||
if (f) {
|
||||
term.refresh(0, term.getOption('rows') - 1);
|
||||
term.refresh(0, term.options.rows! - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
// Another request may have come in while we were waiting, so make
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.FAILED;
|
||||
font = undefined;
|
||||
loadError = e;
|
||||
|
||||
@@ -68,7 +68,7 @@ function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[0-9a-fA-F]/.test(char)) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
@@ -107,7 +107,7 @@ function parseIdentifier(context: IParseContext): string {
|
||||
while (context.offset < context.input.length) {
|
||||
const char = context.input[context.offset++];
|
||||
if (escaped) {
|
||||
if (/[0-9a-fA-F]/.test(char)) {
|
||||
if (/[\dA-Fa-f]/.test(char)) {
|
||||
// Unicode escape
|
||||
context.offset--;
|
||||
str += parseUnicode(context);
|
||||
@@ -156,7 +156,7 @@ function parseUnicode(context: IParseContext): string {
|
||||
// of the escape and is swallowed.
|
||||
return unicodeToString(str);
|
||||
}
|
||||
if (str.length >= 6 || !/[0-9a-fA-F]/.test(char)) {
|
||||
if (str.length >= 6 || !/[\dA-Fa-f]/.test(char)) {
|
||||
// If the next character is not a valid hex digit or we have reached the
|
||||
// maximum of 6 digits in the escape, terminate the escape.
|
||||
context.offset--;
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "../out",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "../out",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" }
|
||||
]
|
||||
}
|
||||
@@ -30,7 +30,18 @@ module.exports = {
|
||||
},
|
||||
mode: 'production',
|
||||
externals: {
|
||||
'font-finder':'font-finder',
|
||||
'font-ligatures':'font-ligatures'
|
||||
'font-finder': 'font-finder',
|
||||
'stream': 'stream',
|
||||
'os': 'os',
|
||||
'util': 'util'
|
||||
},
|
||||
resolve: {
|
||||
// The ligature modules contains fallbacks for node environments, we never want to browserify them
|
||||
fallback: {
|
||||
stream: false,
|
||||
util: false,
|
||||
os: false,
|
||||
path: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -45,23 +45,17 @@ array-from@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195"
|
||||
integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=
|
||||
|
||||
axios@^0.18.0:
|
||||
version "0.18.1"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.1.tgz#ff3f0de2e7b5d180e757ad98000f1081b87bcea3"
|
||||
axios@^0.21.2:
|
||||
version "0.21.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
|
||||
integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==
|
||||
dependencies:
|
||||
follow-redirects "1.5.10"
|
||||
is-buffer "^2.0.2"
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
buffer-crc32@~0.2.3:
|
||||
version "0.2.13"
|
||||
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
|
||||
|
||||
debug@=3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
diff@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
|
||||
@@ -72,26 +66,33 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
dependencies:
|
||||
debug "=3.1.0"
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.14.8"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc"
|
||||
integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==
|
||||
|
||||
font-finder@^1.0.3, font-finder@^1.0.4:
|
||||
font-finder@^1.0.3:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.0.4.tgz#2ca944954dd8d0e1b5bdc4c596cc08607761d89b"
|
||||
dependencies:
|
||||
get-system-fonts "^2.0.0"
|
||||
promise-stream-reader "^1.0.1"
|
||||
|
||||
font-ligatures@^1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/font-ligatures/-/font-ligatures-1.3.3.tgz#63fff18dc8adb3a11fe5eec1f4e8d7edfa8075b9"
|
||||
integrity sha512-NSGpHgVNX81M7AWS1XylK1UZbN3QllfUIDAAuPv6TUcl5O2b781JcKS5L2RopAU0AqlTyX3ZuX/04eaMpbVrHA==
|
||||
font-finder@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
|
||||
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
|
||||
dependencies:
|
||||
get-system-fonts "^2.0.0"
|
||||
promise-stream-reader "^1.0.1"
|
||||
|
||||
font-ligatures@^1.4.1:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.1.tgz#e8c9df7cf6bee80c0cca4a9a578618c7b1f5d8de"
|
||||
integrity sha512-7W6zlfyhvCqShZ5ReUWqmSd9vBaUudW0Hxis+tqUjtHhsPU+L3Grf8mcZAtCiXHTzorhwdRTId2WeH/88gdFkw==
|
||||
dependencies:
|
||||
font-finder "^1.0.3"
|
||||
lru-cache "^4.1.3"
|
||||
lru-cache "^6.0.0"
|
||||
opentype.js "^0.8.0"
|
||||
|
||||
get-system-fonts@^2.0.0:
|
||||
@@ -102,10 +103,6 @@ has-flag@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
|
||||
|
||||
is-buffer@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725"
|
||||
|
||||
isarray@0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
|
||||
@@ -120,9 +117,9 @@ lodash.get@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
|
||||
lodash@^4.17.15:
|
||||
version "4.17.19"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b"
|
||||
integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
lolex@^2.7.5:
|
||||
version "2.7.5"
|
||||
@@ -136,17 +133,17 @@ lolex@^5.0.1:
|
||||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
lru-cache@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
|
||||
lru-cache@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94"
|
||||
integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
|
||||
dependencies:
|
||||
pseudomap "^1.0.2"
|
||||
yallist "^2.1.2"
|
||||
yallist "^4.0.0"
|
||||
|
||||
minimist@^1.2.5:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
mkdirp@0.5.5:
|
||||
version "0.5.5"
|
||||
@@ -155,10 +152,6 @@ mkdirp@0.5.5:
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
nise@^1.4.5:
|
||||
version "1.5.3"
|
||||
resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7"
|
||||
@@ -190,10 +183,6 @@ promise-stream-reader@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649"
|
||||
|
||||
pseudomap@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
|
||||
|
||||
sinon@6.3.5:
|
||||
version "6.3.5"
|
||||
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
|
||||
@@ -224,9 +213,10 @@ type-detect@4.0.8, type-detect@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
|
||||
yallist@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
|
||||
yallist@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"
|
||||
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
|
||||
|
||||
yauzl@^2.10.0:
|
||||
version "2.10.0"
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-search",
|
||||
"version": "0.7.0",
|
||||
"version": "0.9.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,8 +9,13 @@
|
||||
"types": "typings/xterm-addon-search.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -1,197 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('Search Tests', function(): void {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page);
|
||||
await page.evaluate(`window.search = new SearchAddon();`);
|
||||
await page.evaluate(`window.term.loadAddon(window.search);`);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`window.term.reset()`);
|
||||
});
|
||||
|
||||
it('Simple Search', async () => {
|
||||
await writeSync(page, 'dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
|
||||
});
|
||||
|
||||
it('Scrolling Search', async () => {
|
||||
let dataString = '';
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (i === 52) {
|
||||
dataString += '$^1_3{}test$#';
|
||||
}
|
||||
dataString += makeData(50);
|
||||
}
|
||||
await writeSync(page, dataString);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
|
||||
});
|
||||
it('Incremental Find Previous', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
||||
await writeSync(page, 'package.json pack package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it('Incremental Find Next', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
||||
await writeSync(page, 'package.jsonc');
|
||||
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it('Simple Regex', async () => {
|
||||
await writeSync(page, 'abc123defABCD');
|
||||
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
|
||||
});
|
||||
|
||||
it('Search for single result twice should not unselect it', async () => {
|
||||
await writeSync(page, 'abc def');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
});
|
||||
|
||||
describe('Regression tests', () => {
|
||||
describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
before(async () => {
|
||||
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
|
||||
fixture = rawFixture.toString()
|
||||
.replace(/\n/g, '\\n\\r')
|
||||
.replace(/'/g, '\\\'');
|
||||
});
|
||||
it('should find all occurrences using findNext', async () => {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
});
|
||||
it('should find all occurrences using findPrevious', async () => {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeData(length: number): string {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -3,13 +3,25 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
incremental?: boolean;
|
||||
decorations?: ISearchDecorationOptions;
|
||||
noScroll?: boolean;
|
||||
}
|
||||
|
||||
interface ISearchDecorationOptions {
|
||||
matchBackground?: string;
|
||||
matchBorder?: string;
|
||||
matchOverviewRuler: string;
|
||||
activeMatchBackground?: string;
|
||||
activeMatchBorder?: string;
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
export interface ISearchPosition {
|
||||
@@ -21,29 +33,93 @@ export interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
row: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
|
||||
type LineCacheEntry = [
|
||||
/**
|
||||
* The string representation of a line (as opposed to the buffer cell representation).
|
||||
*/
|
||||
lineAsString: string,
|
||||
/**
|
||||
* The offsets where each line starts when the entry describes a wrapped line.
|
||||
*/
|
||||
lineOffsets: number[]
|
||||
];
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _selectedDecoration: IDecoration | undefined;
|
||||
private _resultDecorations: Map<number, IDecoration[]> | undefined;
|
||||
private _searchResults: Map<string, ISearchResult> | undefined;
|
||||
private _onDataDisposable: IDisposable | undefined;
|
||||
private _onResizeDisposable: IDisposable | undefined;
|
||||
private _lastSearchOptions: ISearchOptions | undefined;
|
||||
private _highlightTimeout: number | undefined;
|
||||
/**
|
||||
* 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[] | undefined;
|
||||
private _linesCache: LineCacheEntry[] | undefined;
|
||||
private _linesCacheTimeoutId = 0;
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
private _resultIndex: number | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
|
||||
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
private _updateMatches(): void {
|
||||
if (this._highlightTimeout) {
|
||||
window.clearTimeout(this._highlightTimeout);
|
||||
}
|
||||
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
|
||||
this._highlightTimeout = setTimeout(() => {
|
||||
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}
|
||||
|
||||
public clearDecorations(retainCachedSearchTerm?: boolean): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._searchResults?.clear();
|
||||
this._resultDecorations?.forEach(decorations => {
|
||||
for (const d of decorations) {
|
||||
d.dispose();
|
||||
}
|
||||
});
|
||||
this._resultDecorations?.clear();
|
||||
this._searchResults = undefined;
|
||||
this._resultDecorations = undefined;
|
||||
if (!retainCachedSearchTerm) {
|
||||
this._cachedSearchTerm = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
public clearActiveDecoration(): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._selectedDecoration = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
@@ -56,12 +132,107 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
|
||||
}
|
||||
|
||||
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
this.clearDecorations();
|
||||
return;
|
||||
}
|
||||
searchOptions = searchOptions || {};
|
||||
|
||||
// new search, clear out the old decorations
|
||||
this.clearDecorations(true);
|
||||
this._searchResults = new Map<string, ISearchResult>();
|
||||
this._resultDecorations = new Map<number, IDecoration[]>();
|
||||
const resultDecorations = this._resultDecorations;
|
||||
let result = this._find(term, 0, 0, searchOptions);
|
||||
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
|
||||
this._searchResults.set(`${result.row}-${result.col}`, result);
|
||||
result = this._find(
|
||||
term,
|
||||
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
|
||||
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
|
||||
searchOptions
|
||||
);
|
||||
if (this._searchResults.size > 1000) {
|
||||
this.clearDecorations();
|
||||
this._resultIndex = undefined;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._searchResults.forEach(result => {
|
||||
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
|
||||
if (resultDecoration) {
|
||||
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
|
||||
decorationsForLine.push(resultDecoration);
|
||||
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
return undefined;
|
||||
}
|
||||
if (startCol > this._terminal.cols) {
|
||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
||||
}
|
||||
|
||||
let result: ISearchResult | undefined = undefined;
|
||||
|
||||
this._initLinesCache();
|
||||
|
||||
const searchPosition: ISearchPosition = {
|
||||
startRow,
|
||||
startCol
|
||||
};
|
||||
|
||||
// Search startRow
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
|
||||
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
|
||||
searchPosition.startRow = y;
|
||||
searchPosition.startCol = 0;
|
||||
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
||||
// Otherwise, reset beginning column index to zero with set new unwrapped line index
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _findNextAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._cachedSearchTerm = undefined;
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
|
||||
let startCol = 0;
|
||||
let startRow = 0;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
@@ -83,7 +254,6 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
// Search startRow
|
||||
let result = this._findInLine(term, searchPosition, searchOptions);
|
||||
|
||||
// Search from startRow + 1 to end
|
||||
if (!result) {
|
||||
|
||||
@@ -117,10 +287,21 @@ export class SearchAddon implements ITerminalAddon {
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined) {
|
||||
this._resultIndex = 0;
|
||||
} else {
|
||||
this._resultIndex++;
|
||||
if (this._resultIndex >= this._searchResults.size) {
|
||||
this._resultIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
@@ -132,16 +313,49 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
|
||||
}
|
||||
|
||||
if (!term || term.length === 0) {
|
||||
this._terminal.clearSelection();
|
||||
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
|
||||
} else {
|
||||
this._onDidChangeResults.fire(undefined);
|
||||
}
|
||||
}
|
||||
this._cachedSearchTerm = term;
|
||||
return found;
|
||||
}
|
||||
|
||||
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
let result: ISearchResult | undefined;
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
result = undefined;
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
const isReverseSearch = true;
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
|
||||
let startCol = this._terminal.cols;
|
||||
let result: ISearchResult | undefined;
|
||||
const isReverseSearch = true;
|
||||
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
@@ -158,8 +372,15 @@ export class SearchAddon implements ITerminalAddon {
|
||||
};
|
||||
|
||||
if (incremental) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
if (!(result && result.row === startRow && result.col === startCol)) {
|
||||
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
|
||||
if (!isOldResultHighlighted) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
if (currentSelection) {
|
||||
searchPosition.startRow = currentSelection.endRow;
|
||||
searchPosition.startCol = currentSelection.endColumn;
|
||||
}
|
||||
result = this._findInLine(term, searchPosition, searchOptions, true);
|
||||
}
|
||||
} else {
|
||||
@@ -188,11 +409,24 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
} else {
|
||||
this._resultIndex--;
|
||||
if (this._resultIndex === -1) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is only one result, return true.
|
||||
if (!result && currentSelection) return true;
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result);
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,8 +467,8 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* @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)));
|
||||
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
|
||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,16 +479,17 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* @param term The search term.
|
||||
* @param position The position to start the search.
|
||||
* @param searchOptions Search options.
|
||||
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
||||
const terminal = this._terminal!;
|
||||
let row = searchPosition.startRow;
|
||||
const row = searchPosition.startRow;
|
||||
const col = searchPosition.startCol;
|
||||
|
||||
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||
const firstLine = terminal.buffer.active.getLine(row);
|
||||
if (firstLine && firstLine.isWrapped) {
|
||||
if (firstLine?.isWrapped) {
|
||||
if (isReverseSearch) {
|
||||
searchPosition.startCol += terminal.cols;
|
||||
return;
|
||||
@@ -266,14 +501,16 @@ export class SearchAddon implements ITerminalAddon {
|
||||
searchPosition.startCol += terminal.cols;
|
||||
return this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
let stringLine = this._linesCache ? this._linesCache[row] : void 0;
|
||||
if (stringLine === void 0) {
|
||||
stringLine = this._translateBufferLineToStringWithWrap(row, true);
|
||||
let cache = this._linesCache?.[row];
|
||||
if (!cache) {
|
||||
cache = this._translateBufferLineToStringWithWrap(row, true);
|
||||
if (this._linesCache) {
|
||||
this._linesCache[row] = stringLine;
|
||||
this._linesCache[row] = cache;
|
||||
}
|
||||
}
|
||||
const [stringLine, offsets] = cache;
|
||||
|
||||
const offset = this._bufferColsToStringOffset(row, col);
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
|
||||
@@ -282,68 +519,109 @@ export class SearchAddon implements ITerminalAddon {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
let foundTerm: RegExpExecArray | null;
|
||||
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))) {
|
||||
// This loop will get the resultIndex of the _last_ regex match in the range 0..offset
|
||||
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, offset))) {
|
||||
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||
term = foundTerm[0];
|
||||
searchRegex.lastIndex -= (term.length - 1);
|
||||
}
|
||||
} else {
|
||||
foundTerm = searchRegex.exec(searchStringLine.slice(col));
|
||||
foundTerm = searchRegex.exec(searchStringLine.slice(offset));
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length);
|
||||
resultIndex = offset + (searchRegex.lastIndex - foundTerm[0].length);
|
||||
term = foundTerm[0];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isReverseSearch) {
|
||||
if (col - searchTerm.length >= 0) {
|
||||
resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length);
|
||||
if (offset - searchTerm.length >= 0) {
|
||||
resultIndex = searchStringLine.lastIndexOf(searchTerm, offset - searchTerm.length);
|
||||
}
|
||||
} else {
|
||||
resultIndex = searchStringLine.indexOf(searchTerm, col);
|
||||
resultIndex = searchStringLine.indexOf(searchTerm, offset);
|
||||
}
|
||||
}
|
||||
|
||||
if (resultIndex >= 0) {
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (resultIndex >= terminal.cols) {
|
||||
row += Math.floor(resultIndex / terminal.cols);
|
||||
resultIndex = resultIndex % terminal.cols;
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const line = terminal.buffer.active.getLine(row);
|
||||
|
||||
if (line) {
|
||||
for (let i = 0; i < resultIndex; i++) {
|
||||
const cell = line.getCell(i);
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = cell.getChars();
|
||||
if (char.length > 1) {
|
||||
resultIndex -= char.length - 1;
|
||||
}
|
||||
// Adjust the searchIndex for empty characters following wide unicode
|
||||
// chars (eg. CJK)
|
||||
const charWidth = cell.getWidth();
|
||||
if (charWidth === 0) {
|
||||
resultIndex++;
|
||||
}
|
||||
}
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
let startRowOffset = 0;
|
||||
while (startRowOffset < offsets.length - 1 && resultIndex >= offsets[startRowOffset + 1]) {
|
||||
startRowOffset++;
|
||||
}
|
||||
let endRowOffset = startRowOffset;
|
||||
while (endRowOffset < offsets.length - 1 && resultIndex + term.length >= offsets[endRowOffset + 1]) {
|
||||
endRowOffset++;
|
||||
}
|
||||
const startColOffset = resultIndex - offsets[startRowOffset];
|
||||
const endColOffset = resultIndex + term.length - offsets[endRowOffset];
|
||||
const startColIndex = this._stringLengthToBufferSize(row + startRowOffset, startColOffset);
|
||||
const endColIndex = this._stringLengthToBufferSize(row + endRowOffset, endColOffset);
|
||||
const size = endColIndex - startColIndex + terminal.cols * (endRowOffset - startRowOffset);
|
||||
|
||||
return {
|
||||
term,
|
||||
col: resultIndex,
|
||||
row
|
||||
col: startColIndex,
|
||||
row: row + startRowOffset,
|
||||
size
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _stringLengthToBufferSize(row: number, offset: number): number {
|
||||
const line = this._terminal!.buffer.active.getLine(row);
|
||||
if (!line) {
|
||||
return 0;
|
||||
}
|
||||
for (let i = 0; i < offset; i++) {
|
||||
const cell = line.getCell(i);
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = cell.getChars();
|
||||
if (char.length > 1) {
|
||||
offset -= char.length - 1;
|
||||
}
|
||||
// Adjust the searchIndex for empty characters following wide unicode
|
||||
// chars (eg. CJK)
|
||||
const nextCell = line.getCell(i + 1);
|
||||
if (nextCell && nextCell.getWidth() === 0) {
|
||||
offset++;
|
||||
}
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
private _bufferColsToStringOffset(startRow: number, cols: number): number {
|
||||
const terminal = this._terminal!;
|
||||
let lineIndex = startRow;
|
||||
let offset = 0;
|
||||
let line = terminal.buffer.active.getLine(lineIndex);
|
||||
while (cols > 0 && line) {
|
||||
for (let i = 0; i < cols && i < terminal.cols; i++) {
|
||||
const cell = line.getCell(i);
|
||||
if (!cell) {
|
||||
break;
|
||||
}
|
||||
if (cell.getWidth()) {
|
||||
// Treat null characters as whitespace to align with the translateToString API
|
||||
offset += cell.getCode() === 0 ? 1 : cell.getChars().length;
|
||||
}
|
||||
}
|
||||
lineIndex++;
|
||||
line = terminal.buffer.active.getLine(lineIndex);
|
||||
if (line && !line.isWrapped) {
|
||||
break;
|
||||
}
|
||||
cols -= terminal.cols;
|
||||
}
|
||||
return offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -352,43 +630,123 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* @param line The line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
*/
|
||||
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
|
||||
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
|
||||
const terminal = this._terminal!;
|
||||
let lineString = '';
|
||||
let lineWrapsToNext: boolean;
|
||||
|
||||
do {
|
||||
const strings = [];
|
||||
const lineOffsets = [0];
|
||||
let line = terminal.buffer.active.getLine(lineIndex);
|
||||
while (line) {
|
||||
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
|
||||
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||
const line = terminal.buffer.active.getLine(lineIndex);
|
||||
if (!line) {
|
||||
const lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||
let string = line.translateToString(!lineWrapsToNext && trimRight);
|
||||
if (lineWrapsToNext && nextLine) {
|
||||
const lastCell = line.getCell(line.length - 1);
|
||||
const lastCellIsNull = lastCell && lastCell.getCode() === 0 && lastCell.getWidth() === 1;
|
||||
// a wide character wrapped to the next line
|
||||
if (lastCellIsNull && nextLine.getCell(0)?.getWidth() === 2) {
|
||||
string = string.slice(0, -1);
|
||||
}
|
||||
}
|
||||
strings.push(string);
|
||||
if (lineWrapsToNext) {
|
||||
lineOffsets.push(lineOffsets[lineOffsets.length - 1] + string.length);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
|
||||
lineIndex++;
|
||||
} while (lineWrapsToNext);
|
||||
|
||||
return lineString;
|
||||
line = nextLine;
|
||||
}
|
||||
return [strings.join(''), lineOffsets];
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
* @return Whethera result was selected.
|
||||
* @return Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined): boolean {
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
const terminal = this._terminal!;
|
||||
this.clearActiveDecoration();
|
||||
if (!result) {
|
||||
terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
terminal.select(result.col, result.row, result.term.length);
|
||||
// If it is not in the viewport then we scroll else it just gets selected
|
||||
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
|
||||
let scroll = result.row - terminal.buffer.active.viewportY;
|
||||
scroll = scroll - Math.floor(terminal.rows / 2);
|
||||
terminal.scrollLines(scroll);
|
||||
terminal.select(result.col, result.row, result.size);
|
||||
if (options) {
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (marker) {
|
||||
this._selectedDecoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.activeMatchBackground,
|
||||
layer: 'top',
|
||||
overviewRulerOptions: {
|
||||
color: options.activeMatchColorOverviewRuler
|
||||
}
|
||||
});
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
|
||||
this._selectedDecoration?.onDispose(() => marker.dispose());
|
||||
}
|
||||
}
|
||||
|
||||
if (!noScroll) {
|
||||
// If it is not in the viewport then we scroll else it just gets selected
|
||||
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
|
||||
let scroll = result.row - terminal.buffer.active.viewportY;
|
||||
scroll -= Math.floor(terminal.rows / 2);
|
||||
terminal.scrollLines(scroll);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies styles to the decoration when it is rendered
|
||||
* @param element the decoration's element
|
||||
* @param backgroundColor the background color to apply
|
||||
* @param borderColor the border color to apply
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
if (element.clientWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!element.classList.contains('xterm-find-result-decoration')) {
|
||||
element.classList.add('xterm-find-result-decoration');
|
||||
if (borderColor) {
|
||||
element.style.outline = `1px solid ${borderColor}`;
|
||||
}
|
||||
}
|
||||
if (isActiveResult) {
|
||||
element.classList.add('xterm-find-active-result-decoration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a decoration for the result and applies styles
|
||||
* @param result the search result for which to create the decoration
|
||||
* @param options the options for the decoration
|
||||
* @returns the {@link IDecoration} or undefined if the marker has already been disposed of
|
||||
*/
|
||||
private _createResultDecoration(result: ISearchResult, options: ISearchDecorationOptions): IDecoration | undefined {
|
||||
const terminal = this._terminal!;
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (!marker) {
|
||||
return undefined;
|
||||
}
|
||||
const findResultDecoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.matchBackground,
|
||||
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
|
||||
color: options.matchOverviewRuler,
|
||||
position: 'center'
|
||||
}
|
||||
});
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
|
||||
findResultDecoration?.onDispose(() => marker.dispose());
|
||||
return findResultDecoration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,21 @@
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, launchBrowser, timeout } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('Search Tests', function (): void {
|
||||
before(async function (): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page);
|
||||
});
|
||||
|
||||
after(() => {
|
||||
browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.evaluate(`
|
||||
window.term.reset()
|
||||
window.search?.dispose();
|
||||
window.search = new SearchAddon();
|
||||
window.term.loadAddon(window.search);
|
||||
`);
|
||||
});
|
||||
|
||||
it('Simple Search', async () => {
|
||||
await writeSync(page, 'dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
|
||||
});
|
||||
|
||||
it('Scrolling Search', async () => {
|
||||
let dataString = '';
|
||||
for (let i = 0; i < 100; i++) {
|
||||
if (i === 52) {
|
||||
dataString += '$^1_3{}test$#';
|
||||
}
|
||||
dataString += makeData(50);
|
||||
}
|
||||
await writeSync(page, dataString);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
|
||||
});
|
||||
it('Incremental Find Previous', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
||||
await writeSync(page, 'package.json pack package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it('Incremental Find Next', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
||||
await writeSync(page, 'package.jsonc');
|
||||
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
});
|
||||
it('Simple Regex', async () => {
|
||||
await writeSync(page, 'abc123defABCD');
|
||||
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'ABCD');
|
||||
});
|
||||
|
||||
it('Search for single result twice should not unselect it', async () => {
|
||||
await writeSync(page, 'abc def');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||
});
|
||||
|
||||
it('Search for result bounding with wide unicode chars', async () => {
|
||||
await writeSync(page, '中文xx𝄞𝄞');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('中')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '中');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('xx')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'xx');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '𝄞');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 7,
|
||||
endColumn: 8
|
||||
});
|
||||
});
|
||||
|
||||
describe('onDidChangeResults', async () => {
|
||||
describe('findNext', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('findPrevious', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a')`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 0);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate('window.calls.length'), 1);
|
||||
});
|
||||
it('should fire with correct event values', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
await timeout(2000);
|
||||
assert.strictEqual(await page.evaluate(`debugger; window.search.findPrevious('d', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('c', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 },
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 3, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
});
|
||||
it('should fire with correct event values (incremental)', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('ab', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abc', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Regression tests', () => {
|
||||
describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
before(async () => {
|
||||
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
|
||||
if (process.platform === 'win32') {
|
||||
fixture = rawFixture.toString()
|
||||
.replace(/\n/g, '\\n')
|
||||
.replace(/\r/g, '\\r');
|
||||
} else {
|
||||
fixture = rawFixture.toString()
|
||||
.replace(/\n/g, '\\n\\r');
|
||||
}
|
||||
fixture = fixture
|
||||
.replace(/'/g, `\\'`);
|
||||
});
|
||||
it('should find all occurrences using findNext', async () => {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
});
|
||||
it('should find all occurrences using findPrevious', async () => {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('#3834 lines with null characters before search terms', () => {
|
||||
// This case can be triggered by the prompt when using starship under conpty
|
||||
it('should find all matches on a line containing null characters', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
||||
await writeSync(page, '\\x1b[CHi Hi');
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function makeData(length: number): string {
|
||||
let result = '';
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * characters.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
+63
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, IDisposable, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-search' {
|
||||
/**
|
||||
@@ -32,6 +32,47 @@ declare module 'xterm-addon-search' {
|
||||
* `findNext`, not `findPrevious`.
|
||||
*/
|
||||
incremental?: boolean;
|
||||
|
||||
/**
|
||||
* When set, will highlight all instances of the word on search and show
|
||||
* them in the overview ruler if it's enabled.
|
||||
*/
|
||||
decorations?: ISearchDecorationOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for showing decorations when searching.
|
||||
*/
|
||||
interface ISearchDecorationOptions {
|
||||
/**
|
||||
* The background color of a match, this must use #RRGGBB format.
|
||||
*/
|
||||
matchBackground?: string;
|
||||
|
||||
/**
|
||||
* The border color of a match.
|
||||
*/
|
||||
matchBorder?: string;
|
||||
|
||||
/**
|
||||
* The overview ruler color of a match.
|
||||
*/
|
||||
matchOverviewRuler: string;
|
||||
|
||||
/**
|
||||
* The background color for the currently active match, this must use #RRGGBB format.
|
||||
*/
|
||||
activeMatchBackground?: string;
|
||||
|
||||
/**
|
||||
* The border color of the currently active match.
|
||||
*/
|
||||
activeMatchBorder?: string;
|
||||
|
||||
/**
|
||||
* The overview ruler color of the currently active match.
|
||||
*/
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,5 +105,26 @@ declare module 'xterm-addon-search' {
|
||||
* @param searchOptions The options for the search.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean;
|
||||
|
||||
/**
|
||||
* Clears the decorations and selection
|
||||
*/
|
||||
public clearDecorations(): void;
|
||||
|
||||
/**
|
||||
* Clears the active result decoration, this decoration is applied on top of the selection so
|
||||
* removing it will reveal the selection underneath. This is intended to be called on the search
|
||||
* textarea's `blur` event.
|
||||
*/
|
||||
public clearActiveDecoration(): void;
|
||||
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* @returns -1 for resultIndex for a resultCount of 0
|
||||
* and @returns undefined when the threshold of 1k results
|
||||
* is exceeded and decorations are disposed of.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,13 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: ['./node_modules'],
|
||||
extensions: [ '.js' ],
|
||||
alias: {
|
||||
common: path.resolve('../../out/common')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -16,12 +16,12 @@ class TestTerminal extends Terminal {
|
||||
}
|
||||
}
|
||||
|
||||
perfContext('Terminal: sh -c "dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f"', () => {
|
||||
perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolcat -f"', () => {
|
||||
let content = '';
|
||||
let contentUtf8: Uint8Array;
|
||||
|
||||
before(async () => {
|
||||
const p = spawn('sh', ['-c', 'dd if=/dev/random count=40 bs=1k | hexdump | lolcat -f'], {
|
||||
const p = spawn('sh', ['-c', 'dd if=/dev/urandom count=40 bs=1k | hexdump | lolcat -f'], {
|
||||
name: 'xterm-256color',
|
||||
cols: 80,
|
||||
rows: 25,
|
||||
@@ -35,7 +35,7 @@ perfContext('Terminal: sh -c "dd if=/dev/random count=40 bs=1k | hexdump | lolca
|
||||
chunks.push(data as unknown as Buffer);
|
||||
length += data.length;
|
||||
});
|
||||
await new Promise(resolve => p.on('exit', () => resolve()));
|
||||
await new Promise<void>(resolve => p.on('exit', () => resolve()));
|
||||
contentUtf8 = Buffer.concat(chunks, length);
|
||||
// translate to content string
|
||||
const buffer = new Uint32Array(contentUtf8.length);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"SerializeAddon": ["../src/SerializeAddon"]
|
||||
}
|
||||
},
|
||||
"include": ["../**/*", "../../../typings/xterm.d.ts", "../../../out/**/*"],
|
||||
"include": ["../**/*", "../../../typings/xterm.d.ts"],
|
||||
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||
"references": [
|
||||
{ "path": "../../../src/common" },
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.4.0",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-serialize.js",
|
||||
"types": "typings/xterm-addon-serialize.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package",
|
||||
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
|
||||
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/addons/xterm-addon-serialize/benchmark/*benchmark.js"
|
||||
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
|
||||
@@ -1,369 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
describe('SerializeAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' });
|
||||
await page.evaluate(`
|
||||
window.serializeAddon = new SerializeAddon();
|
||||
window.term.loadAddon(window.serializeAddon);
|
||||
`);
|
||||
});
|
||||
|
||||
after(async () => await browser.close());
|
||||
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
||||
|
||||
it('empty content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
||||
});
|
||||
|
||||
it('trim last empty lines', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const lines = [
|
||||
'',
|
||||
'',
|
||||
digitsString(cols),
|
||||
digitsString(cols),
|
||||
'',
|
||||
'',
|
||||
digitsString(cols),
|
||||
digitsString(cols),
|
||||
'',
|
||||
'',
|
||||
''
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n'));
|
||||
});
|
||||
|
||||
it('digits content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
const digitsLine = digitsString(cols);
|
||||
const lines = newArray<string>(digitsLine, rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize half rows of content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const halfRows = rows >> 1;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize 0 rows of content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), '');
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const color16 = [
|
||||
30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color
|
||||
90, 91, 92, 93, 94, 95, 96, 97,
|
||||
40, 41, 42, 43, 44, 45, 46, 47, // Set background color
|
||||
100, 101, 103, 104, 105, 106, 107
|
||||
];
|
||||
const rows = color16.length;
|
||||
const lines = newArray<string>(
|
||||
(index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`),
|
||||
rows
|
||||
);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with fg/bg flags', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
|
||||
mkSGR(INVERSE) + line,
|
||||
mkSGR(BOLD) + line,
|
||||
mkSGR(UNDERLINED) + line,
|
||||
mkSGR(BLINK) + line,
|
||||
mkSGR(INVISIBLE) + line,
|
||||
mkSGR(NO_INVERSE) + line,
|
||||
mkSGR(NO_BOLD) + line,
|
||||
mkSGR(NO_UNDERLINED) + line,
|
||||
mkSGR(NO_BLINK) + line,
|
||||
mkSGR(NO_INVISIBLE) + line
|
||||
];
|
||||
const rows = lines.length;
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256', async function(): Promise<any> {
|
||||
const rows = 32;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>(
|
||||
(index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`),
|
||||
rows
|
||||
);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_P16_RED) + line, // fg Red,
|
||||
mkSGR(UNDERLINED) + line, // fg Red, Underlined
|
||||
mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
mkSGR(NO_INVERSE) + line, // fg Green, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse
|
||||
mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse
|
||||
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||
mkSGR(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16 and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_P16_RED) + line, // fg Red
|
||||
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
mkSGR(BG_RESET) + line, // Italic
|
||||
mkSGR(NORMAL) + line, // Back to normal
|
||||
mkSGR(FG_P16_RED) + line, // fg Red
|
||||
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
mkSGR(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256 and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_P256_RED) + line, // fg Red 256,
|
||||
mkSGR(UNDERLINED) + line, // fg Red 256, Underlined
|
||||
mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse
|
||||
mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse
|
||||
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||
mkSGR(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256 and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_P256_RED) + line, // fg Red 256
|
||||
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
mkSGR(BG_RESET) + line, // Italic
|
||||
mkSGR(NORMAL) + line, // Back to normal
|
||||
mkSGR(FG_P256_RED) + line, // fg Red 256
|
||||
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
mkSGR(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with colorRGB and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_RGB_RED) + line, // fg Red RGB,
|
||||
mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined
|
||||
mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined
|
||||
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse
|
||||
mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse
|
||||
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||
mkSGR(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with colorRGB and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
mkSGR(FG_RGB_RED) + line, // fg Red RGB
|
||||
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
mkSGR(BG_RESET) + line, // Italic
|
||||
mkSGR(NORMAL) + line, // Back to normal
|
||||
mkSGR(FG_RGB_RED) + line, // fg Red RGB
|
||||
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
mkSGR(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize tabs correctly', async () => {
|
||||
const lines = [
|
||||
'a\tb',
|
||||
'aa\tc',
|
||||
'aaa\td'
|
||||
];
|
||||
const expected = [
|
||||
'a\x1b[7Cb',
|
||||
'aa\x1b[6Cc',
|
||||
'aaa\x1b[5Cd'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK correctly', async () => {
|
||||
const lines = [
|
||||
'中文中文',
|
||||
'12中文',
|
||||
'中文12',
|
||||
'1中文中文中' // this line is going to be wrapped at last character because it has line length of 11 (1+2*5)
|
||||
];
|
||||
const expected = [
|
||||
'中文中文',
|
||||
'12中文',
|
||||
'中文12',
|
||||
'1中文中文',
|
||||
'中'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK Mixed with tab correctly', async () => {
|
||||
const lines = [
|
||||
'中文\t12' // CJK mixed with tab
|
||||
];
|
||||
const expected = [
|
||||
'中文\x1b[4C12'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
});
|
||||
|
||||
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
||||
const array: T[] = new Array<T>(count);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (typeof initial === 'function') {
|
||||
array[i] = (<(index: number) => T>initial)(i);
|
||||
} else {
|
||||
array[i] = <T>initial;
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function digitsString(length: number, from: number = 0, sgr: string = ''): string {
|
||||
let s = sgr;
|
||||
for (let i = 0; i < length; i++) {
|
||||
s += `${(from++) % 10}`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function mkSGR(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
const NORMAL = '0';
|
||||
|
||||
const FG_P16_RED = '31';
|
||||
const FG_P16_GREEN = '32';
|
||||
const FG_P16_YELLOW = '33';
|
||||
const FG_P256_RED = '38;5;196';
|
||||
const FG_P256_GREEN = '38;5;46';
|
||||
const FG_P256_YELLOW = '38;5;226';
|
||||
const FG_RGB_RED = '38;2;255;0;0';
|
||||
const FG_RGB_GREEN = '38;2;0;255;0';
|
||||
const FG_RGB_YELLOW = '38;2;255;255;0';
|
||||
const FG_RESET = '39';
|
||||
|
||||
|
||||
const BG_P16_RED = '41';
|
||||
const BG_P16_GREEN = '42';
|
||||
const BG_P16_YELLOW = '43';
|
||||
const BG_P256_RED = '48;5;196';
|
||||
const BG_P256_GREEN = '48;5;46';
|
||||
const BG_P256_YELLOW = '48;5;226';
|
||||
const BG_RGB_RED = '48;2;255;0;0';
|
||||
const BG_RGB_GREEN = '48;2;0;255;0';
|
||||
const BG_RGB_YELLOW = '48;2;255;255;0';
|
||||
const BG_RESET = '49';
|
||||
|
||||
const INVERSE = '7';
|
||||
const BOLD = '1';
|
||||
const UNDERLINED = '4';
|
||||
const BLINK = '5';
|
||||
const INVISIBLE = '8';
|
||||
|
||||
const NO_INVERSE = '27';
|
||||
const NO_BOLD = '22';
|
||||
const NO_UNDERLINED = '24';
|
||||
const NO_BLINK = '25';
|
||||
const NO_INVISIBLE = '28';
|
||||
|
||||
const ITALIC = '3';
|
||||
const DIM = '2';
|
||||
|
||||
const NO_ITALIC = '23';
|
||||
const NO_DIM = '22';
|
||||
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
function writeP(terminal: Terminal, data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
}
|
||||
|
||||
class TestSelectionService {
|
||||
private _model: SelectionModel;
|
||||
private _hasSelection: boolean = false;
|
||||
|
||||
constructor(
|
||||
bufferService: IBufferService
|
||||
) {
|
||||
this._model = new SelectionModel(bufferService);
|
||||
}
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public get hasSelection(): boolean { return this._hasSelection; }
|
||||
|
||||
public get selectionStart(): [number, number] | undefined { return this._model.finalSelectionStart; }
|
||||
public get selectionEnd(): [number, number] | undefined { return this._model.finalSelectionEnd; }
|
||||
|
||||
public setSelection(col: number, row: number, length: number): void {
|
||||
this._model.selectionStart = [col, row];
|
||||
this._model.selectionStartLength = length;
|
||||
this._hasSelection = true;
|
||||
}
|
||||
}
|
||||
|
||||
describe('xterm-addon-serialize', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
let serializeAddon: SerializeAddon;
|
||||
let terminal: Terminal;
|
||||
let selectionService: any;
|
||||
|
||||
before(() => {
|
||||
serializeAddon = new SerializeAddon();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return { data: [0, 0, 0, 0xFF] };
|
||||
}
|
||||
});
|
||||
|
||||
terminal = new Terminal({ cols: 10, rows: 2 });
|
||||
terminal.loadAddon(serializeAddon);
|
||||
|
||||
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
cm = new ColorManager(document, false);
|
||||
(terminal as any)._core._colorManager = cm;
|
||||
(terminal as any)._core._selectionService = selectionService;
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
it('restoring cursor styles', async () => {
|
||||
await writeP(terminal, sgr('32') + '> ' + sgr('0'));
|
||||
assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('html', () => {
|
||||
it('empty terminal with selection turned off', () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.notEqual(output, '');
|
||||
assert.equal((output.match(/<div><span> {10}<\/span><\/div>/g) || []).length, 2);
|
||||
});
|
||||
|
||||
it('empty terminal with no selection', () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal(output, '');
|
||||
});
|
||||
|
||||
it('basic terminal with selection', async () => {
|
||||
await writeP(terminal, ' terminal ');
|
||||
terminal.select(1, 0, 8);
|
||||
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
onlySelection: true
|
||||
});
|
||||
assert.equal((output.match(/<div><span>terminal<\/span><\/div>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with bold styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('1') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with italic styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('3') + 'terminal' + sgr('23') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-style: italic;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with inverse styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('7') + 'terminal' + sgr('27') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #000000; background-color: #BFBFBF;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with underline styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('4') + 'terminal' + sgr('24') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: underline;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with invisible styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('8') + 'terminal' + sgr('28') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='visibility: hidden;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with dim styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('2') + 'terminal' + sgr('22') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='opacity: 0.5;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with strikethrough styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('9') + 'terminal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with combined styling', async () => {
|
||||
await writeP(terminal, sgr('1') + ' ' + sgr('9') + 'termi' + sgr('22') + 'nal' + sgr('29') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='font-weight: bold;'> <\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='font-weight: bold; text-decoration: line-through;'>termi<\/span>/g) || []).length, 1, output);
|
||||
assert.equal((output.match(/<span style='text-decoration: line-through;'>nal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with color styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('38;5;46') + 'terminal' + sgr('39') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('cells with background styling', async () => {
|
||||
await writeP(terminal, ' ' + sgr('48;5;46') + 'terminal' + sgr('49') + ' ');
|
||||
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/<span style='background-color: #00ff00;'>terminal<\/span>/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with default options', async () => {
|
||||
const output = serializeAddon.serializeAsHTML();
|
||||
assert.equal((output.match(/color: #000000; background-color: #ffffff; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with custom options', async () => {
|
||||
terminal.options.fontFamily = 'verdana';
|
||||
terminal.options.fontSize = 20;
|
||||
terminal.options.theme = {
|
||||
foreground: '#ff00ff',
|
||||
background: '#00ff00'
|
||||
};
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ff00ff; background-color: #00ff00; font-family: verdana; font-size: 20px;/g) || []).length, 1, output);
|
||||
});
|
||||
|
||||
it('empty terminal with background included', async () => {
|
||||
const output = serializeAddon.serializeAsHTML({
|
||||
includeGlobalBackground: true
|
||||
});
|
||||
assert.equal((output.match(/color: #ffffff; background-color: #000000; font-family: courier-new, courier, monospace; font-size: 15px;/g) || []).length, 1, output);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,7 +5,9 @@
|
||||
* (EXPERIMENTAL) This Addon is still under development
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell, IBufferRange } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IAttributeData } from 'common/Types';
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
@@ -13,20 +15,30 @@ function constrain(value: number, low: number, high: number): number {
|
||||
|
||||
// TODO: Refine this template class later
|
||||
abstract class BaseSerializeHandler {
|
||||
constructor(private _buffer: IBuffer) { }
|
||||
constructor(
|
||||
protected readonly _buffer: IBuffer
|
||||
) {
|
||||
}
|
||||
|
||||
public serialize(startRow: number, endRow: number): string {
|
||||
public serialize(range: IBufferRange): string {
|
||||
// we need two of them to flip between old and new cell
|
||||
const cell1 = this._buffer.getNullCell();
|
||||
const cell2 = this._buffer.getNullCell();
|
||||
let oldCell = cell1;
|
||||
|
||||
this._beforeSerialize(endRow - startRow);
|
||||
const startRow = range.start.x;
|
||||
const endRow = range.end.x;
|
||||
const startColumn = range.start.y;
|
||||
const endColumn = range.end.y;
|
||||
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row <= endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line) {
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
const startLineColumn = row !== range.start.x ? 0 : startColumn;
|
||||
const endLineColumn = row !== range.end.x ? line.length : endColumn;
|
||||
for (let col = startLineColumn; col < endLineColumn; col++) {
|
||||
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
|
||||
if (!c) {
|
||||
console.warn(`Can't get cell at row=${row}, col=${col}`);
|
||||
@@ -36,7 +48,7 @@ abstract class BaseSerializeHandler {
|
||||
oldCell = c;
|
||||
}
|
||||
}
|
||||
this._rowEnd(row);
|
||||
this._rowEnd(row, row === endRow);
|
||||
}
|
||||
|
||||
this._afterSerialize();
|
||||
@@ -45,53 +57,180 @@ abstract class BaseSerializeHandler {
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||
protected _rowEnd(row: number): void { }
|
||||
protected _beforeSerialize(rows: number): void { }
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||
protected _afterSerialize(): void { }
|
||||
protected _serializeString(): string { return ''; }
|
||||
}
|
||||
|
||||
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getFgColorMode() === cell2.getFgColorMode()
|
||||
&& cell1.getFgColor() === cell2.getFgColor();
|
||||
}
|
||||
|
||||
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalBg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.getBgColorMode() === cell2.getBgColorMode()
|
||||
&& cell1.getBgColor() === cell2.getBgColor();
|
||||
}
|
||||
|
||||
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean {
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
&& cell1.isItalic() === cell2.isItalic()
|
||||
&& cell1.isDim() === cell2.isDim();
|
||||
&& cell1.isDim() === cell2.isDim()
|
||||
&& cell1.isStrikethrough() === cell2.isStrikethrough();
|
||||
}
|
||||
|
||||
class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _rowIndex: number = 0;
|
||||
private _allRows: string[] = new Array<string>();
|
||||
private _allRowSeparators: string[] = new Array<string>();
|
||||
private _currentRow: string = '';
|
||||
private _nullCellCount: number = 0;
|
||||
|
||||
constructor(buffer: IBuffer) {
|
||||
// we can see a full colored cell and a null cell that only have background the same style
|
||||
// but the information isn't preserved by null cell itself
|
||||
// so wee need to record it when required.
|
||||
private _cursorStyle: IBufferCell = this._buffer.getNullCell();
|
||||
|
||||
// where exact the cursor styles comes from
|
||||
// because we can't copy the cell directly
|
||||
// so we remember where the content comes from instead
|
||||
private _cursorStyleRow: number = 0;
|
||||
private _cursorStyleCol: number = 0;
|
||||
|
||||
// this is a null cell for reference for checking whether background is empty or not
|
||||
private _backgroundCell: IBufferCell = this._buffer.getNullCell();
|
||||
|
||||
private _firstRow: number = 0;
|
||||
private _lastCursorRow: number = 0;
|
||||
private _lastCursorCol: number = 0;
|
||||
private _lastContentCursorRow: number = 0;
|
||||
private _lastContentCursorCol: number = 0;
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: Terminal
|
||||
) {
|
||||
super(buffer);
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number): void {
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._allRows = new Array<string>(rows);
|
||||
this._lastContentCursorRow = start;
|
||||
this._lastCursorRow = start;
|
||||
this._firstRow = start;
|
||||
}
|
||||
|
||||
protected _rowEnd(row: number): void {
|
||||
this._allRows[this._rowIndex++] = this._currentRow;
|
||||
private _thisRowLastChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block will missing
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// use clear right to set background.
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
}
|
||||
|
||||
let rowSeparator = '';
|
||||
|
||||
// handle row separator
|
||||
if (!isLastRow) {
|
||||
// Enable BCE
|
||||
if (row - this._firstRow >= this._terminal.rows) {
|
||||
this._buffer.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);
|
||||
}
|
||||
|
||||
// Fetch current line
|
||||
const currentLine = this._buffer.getLine(row)!;
|
||||
// Fetch next line
|
||||
const nextLine = this._buffer.getLine(row + 1)!;
|
||||
|
||||
if (!nextLine.isWrapped) {
|
||||
// just insert the line break
|
||||
rowSeparator = '\r\n';
|
||||
// we sended the enter
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
} else {
|
||||
rowSeparator = '';
|
||||
const thisRowLastChar = currentLine.getCell(currentLine.length - 1, this._thisRowLastChar)!;
|
||||
const thisRowLastSecondChar = currentLine.getCell(currentLine.length - 2, this._thisRowLastSecondChar)!;
|
||||
const nextRowFirstChar = nextLine.getCell(0, this._nextRowFirstChar)!;
|
||||
const isNextRowFirstCharDoubleWidth = nextRowFirstChar.getWidth() > 1;
|
||||
|
||||
// validate whether this line wrap is ever possible
|
||||
// which mean whether cursor can placed at a overflow position (x === row) naturally
|
||||
let isValid = false;
|
||||
|
||||
if (
|
||||
// you must output character to cause overflow, control sequence can't do this
|
||||
nextRowFirstChar.getChars() &&
|
||||
isNextRowFirstCharDoubleWidth ? this._nullCellCount <= 1 : this._nullCellCount <= 0
|
||||
) {
|
||||
if (
|
||||
// the last character can't be null,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
|
||||
if (
|
||||
// the second to last character can't be null if the next line starts with CJK,
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
isNextRowFirstCharDoubleWidth &&
|
||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
// force the wrap with magic
|
||||
// insert enough character to force the wrap
|
||||
rowSeparator = '-'.repeat(this._nullCellCount + 1);
|
||||
// move back and erase next line head
|
||||
rowSeparator += '\u001b[1D\u001b[1X';
|
||||
|
||||
if (this._nullCellCount > 0) {
|
||||
// do these because we filled the last several null slot, which we shouldn't
|
||||
rowSeparator += '\u001b[A';
|
||||
rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}C`;
|
||||
rowSeparator += `\u001b[${this._nullCellCount}X`;
|
||||
rowSeparator += `\u001b[${currentLine.length - this._nullCellCount}D`;
|
||||
rowSeparator += '\u001b[B';
|
||||
}
|
||||
|
||||
// This is content and need the be serialized even it is invisible.
|
||||
// without this, wrap will be missing from outputs.
|
||||
this._lastContentCursorRow = row + 1;
|
||||
this._lastContentCursorCol = 0;
|
||||
|
||||
// force commit the cursor position
|
||||
this._lastCursorRow = row + 1;
|
||||
this._lastCursorCol = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._allRows[this._rowIndex] = this._currentRow;
|
||||
this._allRowSeparators[this._rowIndex++] = rowSeparator;
|
||||
this._currentRow = '';
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
private _diffStyle(cell: IBufferCell | IAttributeData, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
@@ -99,7 +238,9 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
if (cell.isAttributeDefault()) {
|
||||
this._currentRow += '\x1b[0m';
|
||||
if (!oldCell.isAttributeDefault()) {
|
||||
sgrSeq.push(0);
|
||||
}
|
||||
} else {
|
||||
if (fgChanged) {
|
||||
const color = cell.getFgColor();
|
||||
@@ -127,34 +268,142 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
if (cell.isInvisible() !== oldCell.isInvisible()) { sgrSeq.push(cell.isInvisible() ? 8 : 28); }
|
||||
if (cell.isItalic() !== oldCell.isItalic()) { sgrSeq.push(cell.isItalic() ? 3 : 23); }
|
||||
if (cell.isDim() !== oldCell.isDim()) { sgrSeq.push(cell.isDim() ? 2 : 22); }
|
||||
if (cell.isStrikethrough() !== oldCell.isStrikethrough()) { sgrSeq.push(cell.isStrikethrough() ? 9 : 29); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sgrSeq.length) {
|
||||
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
|
||||
return sgrSeq;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count number of null cells encountered after the last non-null cell and move the cursor
|
||||
// if a non-null cell is found (eg. \t or cursor move)
|
||||
if (cell.getChars() === '') {
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const sgrSeq = this._diffStyle(cell, this._cursorStyle);
|
||||
|
||||
// the empty cell style is only assumed to be changed when background changed, because foreground is always 0.
|
||||
const styleChanged = isEmptyCell ? !equalBg(this._cursorStyle, cell) : sgrSeq.length > 0;
|
||||
|
||||
/**
|
||||
* handles style change
|
||||
*/
|
||||
if (styleChanged) {
|
||||
// before update the style, we need to fill empty cell back
|
||||
if (this._nullCellCount > 0) {
|
||||
// use clear right to set background.
|
||||
if (!equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
}
|
||||
// use move right to move cursor.
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col;
|
||||
|
||||
this._currentRow += `\u001b[${sgrSeq.join(';')}m`;
|
||||
|
||||
// update the last cursor style
|
||||
const line = this._buffer.getLine(row);
|
||||
if (line !== undefined) {
|
||||
line.getCell(col, this._cursorStyle);
|
||||
this._cursorStyleRow = row;
|
||||
this._cursorStyleCol = col;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles actual content
|
||||
*/
|
||||
if (isEmptyCell) {
|
||||
this._nullCellCount += cell.getWidth();
|
||||
} else if (this._nullCellCount > 0) {
|
||||
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
} else {
|
||||
if (this._nullCellCount > 0) {
|
||||
// we can just assume we have same style with previous one here
|
||||
// because style change is handled by previous stage
|
||||
// use move right when background is empty, use clear right when there is background.
|
||||
if (equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
} else {
|
||||
this._currentRow += `\u001b[${this._nullCellCount}X`;
|
||||
this._currentRow += `\u001b[${this._nullCellCount}C`;
|
||||
}
|
||||
this._nullCellCount = 0;
|
||||
}
|
||||
|
||||
this._currentRow += cell.getChars();
|
||||
this._currentRow += cell.getChars();
|
||||
|
||||
// update cursor
|
||||
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||
this._lastContentCursorCol = this._lastCursorCol = col + cell.getWidth();
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
let rowEnd = this._allRows.length;
|
||||
for (; rowEnd > 0; rowEnd--) {
|
||||
if (this._allRows[rowEnd - 1]) {
|
||||
break;
|
||||
|
||||
// the fixup is only required for data without scrollback
|
||||
// because it will always be placed at last line otherwise
|
||||
if (this._buffer.length - this._firstRow <= this._terminal.rows) {
|
||||
rowEnd = this._lastContentCursorRow + 1 - this._firstRow;
|
||||
this._lastCursorCol = this._lastContentCursorCol;
|
||||
this._lastCursorRow = this._lastContentCursorRow;
|
||||
}
|
||||
|
||||
let content = '';
|
||||
|
||||
for (let i = 0; i < rowEnd; i++) {
|
||||
content += this._allRows[i];
|
||||
if (i + 1 < rowEnd) {
|
||||
content += this._allRowSeparators[i];
|
||||
}
|
||||
}
|
||||
return this._allRows.slice(0, rowEnd).join('\r\n');
|
||||
|
||||
// restore the cursor
|
||||
const realCursorRow = this._buffer.baseY + this._buffer.cursorY;
|
||||
const realCursorCol = this._buffer.cursorX;
|
||||
|
||||
const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol);
|
||||
|
||||
const moveRight = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}C`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}D`;
|
||||
}
|
||||
};
|
||||
const moveDown = (offset: number): void => {
|
||||
if (offset > 0) {
|
||||
content += `\u001b[${offset}B`;
|
||||
} else if (offset < 0) {
|
||||
content += `\u001b[${-offset}A`;
|
||||
}
|
||||
};
|
||||
|
||||
if (cursorMoved) {
|
||||
moveDown(realCursorRow - this._lastCursorRow);
|
||||
moveRight(realCursorCol - this._lastCursorCol);
|
||||
}
|
||||
|
||||
// Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677
|
||||
// HACK: Internal API access since it's awkward to expose this in the API and serialize will
|
||||
// likely be the only consumer
|
||||
const curAttrData: IAttributeData = (this._terminal as any)._core._inputHandler._curAttrData;
|
||||
const sgrSeq = this._diffStyle(curAttrData, this._cursorStyle);
|
||||
if (sgrSeq.length > 0) {
|
||||
content += `\u001b[${sgrSeq.join(';')}m`;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,21 +416,256 @@ export class SerializeAddon implements ITerminalAddon {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
|
||||
public serialize(rows?: number): string {
|
||||
// TODO: Add re-position cursor support
|
||||
// TODO: Add word wrap mode support
|
||||
private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string {
|
||||
const maxRows = buffer.length;
|
||||
const handler = new StringSerializeHandler(buffer, terminal);
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
private _serializeBufferAsHTML(terminal: Terminal, options: Partial<IHTMLSerializeOptions>): string {
|
||||
const buffer = terminal.buffer.active;
|
||||
const handler = new HTMLSerializeHandler(buffer, terminal, options);
|
||||
const onlySelection = options.onlySelection ?? false;
|
||||
if (!onlySelection) {
|
||||
const maxRows = buffer.length;
|
||||
const scrollback = options.scrollback;
|
||||
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows);
|
||||
return handler.serialize({
|
||||
start: { x: maxRows - correctRows, y: 0 },
|
||||
end: { x: maxRows - 1, y: terminal.cols }
|
||||
});
|
||||
}
|
||||
|
||||
const selection = this._terminal?.getSelectionPosition();
|
||||
if (selection !== undefined) {
|
||||
return handler.serialize({
|
||||
start: { x: selection.startRow, y: selection.startColumn },
|
||||
end: { x: selection.endRow, y: selection.endColumn }
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
private _serializeModes(terminal: Terminal): string {
|
||||
let content = '';
|
||||
const modes = terminal.modes;
|
||||
|
||||
// Default: false
|
||||
if (modes.applicationCursorKeysMode) content += '\x1b[?1h';
|
||||
if (modes.applicationKeypadMode) content += '\x1b[?66h';
|
||||
if (modes.bracketedPasteMode) content += '\x1b[?2004h';
|
||||
if (modes.insertMode) content += '\x1b[4h';
|
||||
if (modes.originMode) content += '\x1b[?6h';
|
||||
if (modes.reverseWraparoundMode) content += '\x1b[?45h';
|
||||
if (modes.sendFocusMode) content += '\x1b[?1004h';
|
||||
|
||||
// Default: true
|
||||
if (modes.wraparoundMode === false) content += '\x1b[?7l';
|
||||
|
||||
// Default: 'none'
|
||||
if (modes.mouseTrackingMode !== 'none') {
|
||||
switch (modes.mouseTrackingMode) {
|
||||
case 'x10': content += '\x1b[?9h'; break;
|
||||
case 'vt200': content += '\x1b[?1000h'; break;
|
||||
case 'drag': content += '\x1b[?1002h'; break;
|
||||
case 'any': content += '\x1b[?1003h'; break;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public serialize(options?: ISerializeOptions): string {
|
||||
// TODO: Add combinedData support
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
const maxRows = this._terminal.buffer.active.length;
|
||||
const handler = new StringSerializeHandler(this._terminal.buffer.active);
|
||||
// Normal buffer
|
||||
let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback);
|
||||
|
||||
rows = (rows === undefined) ? maxRows : constrain(rows, 0, maxRows);
|
||||
// Alternate buffer
|
||||
if (!options?.excludeAltBuffer) {
|
||||
if (this._terminal.buffer.active.type === 'alternate') {
|
||||
const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined);
|
||||
content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`;
|
||||
}
|
||||
}
|
||||
|
||||
return handler.serialize(maxRows - rows, maxRows);
|
||||
// Modes
|
||||
if (!options?.excludeModes) {
|
||||
content += this._serializeModes(this._terminal);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
|
||||
return this._serializeBufferAsHTML(this._terminal, options || {});
|
||||
}
|
||||
|
||||
public dispose(): void { }
|
||||
}
|
||||
|
||||
|
||||
interface ISerializeOptions {
|
||||
scrollback?: number;
|
||||
excludeModes?: boolean;
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
interface IHTMLSerializeOptions {
|
||||
scrollback: number;
|
||||
onlySelection: boolean;
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
|
||||
export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
private _currentRow: string = '';
|
||||
|
||||
private _htmlContent = '';
|
||||
|
||||
private _colors: IColorSet;
|
||||
|
||||
constructor(
|
||||
buffer: IBuffer,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _options: Partial<IHTMLSerializeOptions>
|
||||
) {
|
||||
super(buffer);
|
||||
|
||||
// https://github.com/xtermjs/xterm.js/issues/3601
|
||||
this._colors = (_terminal as any)._core._colorManager.colors;
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
targetLength = targetLength >> 0;
|
||||
padString = padString ?? ' ';
|
||||
if (target.length > targetLength) {
|
||||
return target;
|
||||
}
|
||||
|
||||
targetLength -= target.length;
|
||||
if (targetLength > padString.length) {
|
||||
padString += padString.repeat(targetLength / padString.length);
|
||||
}
|
||||
return padString.slice(0, targetLength) + target;
|
||||
}
|
||||
|
||||
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||
this._htmlContent += '<html><body><!--StartFragment--><pre>';
|
||||
|
||||
let foreground = '#000000';
|
||||
let background = '#ffffff';
|
||||
if (this._options.includeGlobalBackground ?? false) {
|
||||
foreground = this._terminal.options.theme?.foreground ?? '#ffffff';
|
||||
background = this._terminal.options.theme?.background ?? '#000000';
|
||||
}
|
||||
|
||||
const globalStyleDefinitions = [];
|
||||
globalStyleDefinitions.push('color: ' + foreground + ';');
|
||||
globalStyleDefinitions.push('background-color: ' + background + ';');
|
||||
globalStyleDefinitions.push('font-family: ' + this._terminal.options.fontFamily + ';');
|
||||
globalStyleDefinitions.push('font-size: ' + this._terminal.options.fontSize + 'px;');
|
||||
this._htmlContent += '<div style=\'' + globalStyleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
protected _afterSerialize(): void {
|
||||
this._htmlContent += '</div>';
|
||||
this._htmlContent += '</pre><!--EndFragment--></body></html>';
|
||||
}
|
||||
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
this._htmlContent += '<div><span>' + this._currentRow + '</span></div>';
|
||||
this._currentRow = '';
|
||||
}
|
||||
|
||||
private _getHexColor(cell: IBufferCell, isFg: boolean): string | undefined {
|
||||
const color = isFg ? cell.getFgColor() : cell.getBgColor();
|
||||
if (isFg ? cell.isFgRGB() : cell.isBgRGB()) {
|
||||
const rgb = [
|
||||
(color >> 16) & 255,
|
||||
(color >> 8) & 255,
|
||||
(color ) & 255
|
||||
];
|
||||
return rgb.map(x => this._padStart(x.toString(16), 2, '0')).join('');
|
||||
}
|
||||
if (isFg ? cell.isFgPalette() : cell.isBgPalette()) {
|
||||
return this._colors.ansi[color].css;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): string[] | undefined {
|
||||
const content: string[] = [];
|
||||
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
const flagsChanged = !equalFlags(cell, oldCell);
|
||||
|
||||
if (fgChanged || bgChanged || flagsChanged) {
|
||||
const fgHexColor = this._getHexColor(cell, true);
|
||||
if (fgHexColor) {
|
||||
content.push('color: ' + fgHexColor + ';');
|
||||
}
|
||||
|
||||
const bgHexColor = this._getHexColor(cell, false);
|
||||
if (bgHexColor) {
|
||||
content.push('background-color: ' + bgHexColor + ';');
|
||||
}
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
if (cell.isDim()) { content.push('opacity: 0.5;'); }
|
||||
if (cell.isStrikethrough()) { content.push('text-decoration: line-through;'); }
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void {
|
||||
// a width 0 cell don't need to be count because it is just a placeholder after a CJK character;
|
||||
const isPlaceHolderCell = cell.getWidth() === 0;
|
||||
if (isPlaceHolderCell) {
|
||||
return;
|
||||
}
|
||||
|
||||
// this cell don't have content
|
||||
const isEmptyCell = cell.getChars() === '';
|
||||
|
||||
const styleDefinitions = this._diffStyle(cell, oldCell);
|
||||
|
||||
// handles style change
|
||||
if (styleDefinitions) {
|
||||
this._currentRow += styleDefinitions.length === 0 ?
|
||||
'</span><span>' :
|
||||
'</span><span style=\'' + styleDefinitions.join(' ') + '\'>';
|
||||
}
|
||||
|
||||
// handles actual content
|
||||
if (isEmptyCell) {
|
||||
this._currentRow += ' ';
|
||||
} else {
|
||||
this._currentRow += cell.getChars();
|
||||
}
|
||||
}
|
||||
|
||||
protected _serializeString(): string {
|
||||
return this._htmlContent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
@@ -29,6 +31,9 @@
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,612 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
const writeRawSync = (page: any, str: string): Promise<void> => writeSync(page, `' +` + JSON.stringify(str) + `+ '`);
|
||||
|
||||
const testNormalScreenEqual = async (page: any, str: string): Promise<void> => {
|
||||
await writeRawSync(page, str);
|
||||
const originalBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, result);
|
||||
const newBuffer = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
// chai decides -0 and 0 are different number...
|
||||
// and firefox have a bug that output -0 for unknown reason
|
||||
assert.equal(JSON.stringify(originalBuffer), JSON.stringify(newBuffer));
|
||||
};
|
||||
|
||||
async function testSerializeEquals(writeContent: string, expectedSerialized: string): Promise<void> {
|
||||
await writeRawSync(page, writeContent);
|
||||
const result = await page.evaluate(`serializeAddon.serialize();`) as string;
|
||||
assert.strictEqual(result, expectedSerialized);
|
||||
}
|
||||
|
||||
describe('SerializeAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' });
|
||||
await page.evaluate(`
|
||||
window.serializeAddon = new SerializeAddon();
|
||||
window.term.loadAddon(window.serializeAddon);
|
||||
window.inspectBuffer = (buffer) => {
|
||||
const lines = [];
|
||||
for (let i = 0; i < buffer.length; i++) {
|
||||
// Do this intentionally to get content of underlining source
|
||||
const bufferLine = buffer.getLine(i)._line;
|
||||
lines.push(JSON.stringify(bufferLine));
|
||||
}
|
||||
return {
|
||||
x: buffer.cursorX,
|
||||
y: buffer.cursorY,
|
||||
data: lines
|
||||
};
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
after(async () => await browser.close());
|
||||
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
||||
|
||||
it('produce different output when we call test util with different text', async function(): Promise<any> {
|
||||
await writeRawSync(page, '12345');
|
||||
const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '67890');
|
||||
const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2));
|
||||
});
|
||||
});
|
||||
|
||||
it('produce different output when we call test util with different line wrap', async function(): Promise<any> {
|
||||
await writeRawSync(page, '1234567890\r\n12345');
|
||||
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
await page.evaluate(`term.reset();`);
|
||||
await writeRawSync(page, '123456789012345');
|
||||
const buffer4 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||
|
||||
assert.throw(() => {
|
||||
assert.equal(JSON.stringify(buffer3), JSON.stringify(buffer4));
|
||||
});
|
||||
});
|
||||
|
||||
it('empty content', async function(): Promise<any> {
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
||||
});
|
||||
|
||||
it('unwrap wrapped line', async function(): Promise<any> {
|
||||
const lines = ['123456789123456789'];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('does not unwrap non-wrapped line', async function(): Promise<any> {
|
||||
const lines = [
|
||||
'123456789',
|
||||
'123456789'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
|
||||
it('preserve last empty lines', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const lines = [
|
||||
'',
|
||||
'',
|
||||
digitsString(cols),
|
||||
digitsString(cols),
|
||||
'',
|
||||
'',
|
||||
digitsString(cols),
|
||||
digitsString(cols),
|
||||
'',
|
||||
'',
|
||||
''
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('digits content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
const digitsLine = digitsString(cols);
|
||||
const lines = newArray<string>(digitsLine, rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize with half of scrollback', async function(): Promise<any> {
|
||||
const rows = 20;
|
||||
const scrollback = rows - 10;
|
||||
const halfScrollback = scrollback / 2;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize 0 rows of scrollback', async function(): Promise<any> {
|
||||
const rows = 20;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: 0 });`), lines.slice(rows - 10, rows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize exclude modes', async () => {
|
||||
await writeSync(page, 'before\\x1b[?1hafter');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'beforeafter\x1b[?1h');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeModes: true });`), 'beforeafter');
|
||||
});
|
||||
|
||||
it('serialize exclude alt buffer', async () => {
|
||||
await writeSync(page, 'normal\\x1b[?1049h\\x1b[Halt');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), 'normal\x1b[?1049h\x1b[Halt');
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ excludeAltBuffer: true });`), 'normal');
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const color16 = [
|
||||
30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color
|
||||
90, 91, 92, 93, 94, 95, 96, 97,
|
||||
40, 41, 42, 43, 44, 45, 46, 47, // Set background color
|
||||
100, 101, 103, 104, 105, 106, 107
|
||||
];
|
||||
const rows = color16.length;
|
||||
const lines = newArray<string>(
|
||||
(index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`),
|
||||
rows
|
||||
);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with fg/bg flags', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
|
||||
sgr(INVERSE) + line,
|
||||
sgr(BOLD) + line,
|
||||
sgr(UNDERLINED) + line,
|
||||
sgr(BLINK) + line,
|
||||
sgr(INVISIBLE) + line,
|
||||
sgr(STRIKETHROUGH) + line,
|
||||
sgr(NO_INVERSE) + line,
|
||||
sgr(NO_BOLD) + line,
|
||||
sgr(NO_UNDERLINED) + line,
|
||||
sgr(NO_BLINK) + line,
|
||||
sgr(NO_INVISIBLE) + line,
|
||||
sgr(NO_STRIKETHROUGH) + line
|
||||
];
|
||||
const rows = lines.length;
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256', async function(): Promise<any> {
|
||||
const rows = 32;
|
||||
const cols = 10;
|
||||
const lines = newArray<string>(
|
||||
(index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`),
|
||||
rows
|
||||
);
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_RED) + line, // fg Red,
|
||||
sgr(UNDERLINED) + line, // fg Red, Underlined
|
||||
sgr(FG_P16_GREEN) + line, // fg Green, Underlined
|
||||
sgr(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green, Underlined
|
||||
sgr(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||
sgr(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color16 and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_RED) + line, // fg Red
|
||||
sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_P16_RED) + line, // fg Red
|
||||
sgr(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||
sgr(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256 and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P256_RED) + line, // fg Red 256,
|
||||
sgr(UNDERLINED) + line, // fg Red 256, Underlined
|
||||
sgr(FG_P256_GREEN) + line, // fg Green 256, Underlined
|
||||
sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green 256, Underlined
|
||||
sgr(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||
sgr(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with color256 and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P256_RED) + line, // fg Red 256
|
||||
sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_P256_RED) + line, // fg Red 256
|
||||
sgr(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||
sgr(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with colorRGB and style separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB,
|
||||
sgr(UNDERLINED) + line, // fg Red RGB, Underlined
|
||||
sgr(FG_RGB_GREEN) + line, // fg Green RGB, Underlined
|
||||
sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
sgr(NO_INVERSE) + line, // fg Green RGB, Underlined
|
||||
sgr(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||
sgr(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse
|
||||
sgr(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse
|
||||
sgr(BG_RESET) + line, // Underlined, Inverse
|
||||
sgr(NORMAL) + line // Back to normal
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with colorRGB and style together', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB
|
||||
sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
sgr(BG_RESET) + line, // Italic
|
||||
sgr(NORMAL) + line, // Back to normal
|
||||
sgr(FG_RGB_RED) + line, // fg Red RGB
|
||||
sgr(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||
sgr(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||
sgr(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||
sgr(BG_RESET) + line // Italic
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize tabs correctly', async () => {
|
||||
const lines = [
|
||||
'a\tb',
|
||||
'aa\tc',
|
||||
'aaa\td'
|
||||
];
|
||||
const expected = [
|
||||
'a\x1b[7Cb',
|
||||
'aa\x1b[6Cc',
|
||||
'aaa\x1b[5Cd'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK correctly', async () => {
|
||||
const lines = [
|
||||
'中文中文',
|
||||
'12中文',
|
||||
'中文12',
|
||||
// This line is going to be wrapped at last character
|
||||
// because it has line length of 11 (1+2*5).
|
||||
// We concat it back without the null cell currently.
|
||||
// But this may be incorrect.
|
||||
// see also #3097
|
||||
'1中文中文中'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK Mixed with tab correctly', async () => {
|
||||
const lines = [
|
||||
'中文\t12' // CJK mixed with tab
|
||||
];
|
||||
const expected = [
|
||||
'中文\x1b[4C12'
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize with alt screen correctly', async () => {
|
||||
const SMCUP = '\u001b[?1049h';
|
||||
const CUP = '\u001b[H';
|
||||
|
||||
const lines = [
|
||||
`1${SMCUP}${CUP}2`
|
||||
];
|
||||
const expected = [
|
||||
`1${SMCUP}${CUP}2`
|
||||
];
|
||||
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate');
|
||||
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||
});
|
||||
|
||||
it('serialize without alt screen correctly', async () => {
|
||||
const SMCUP = '\u001b[?1049h';
|
||||
const RMCUP = '\u001b[?1049l';
|
||||
|
||||
const lines = [
|
||||
`1${SMCUP}2${RMCUP}`
|
||||
];
|
||||
const expected = [
|
||||
`1`
|
||||
];
|
||||
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal');
|
||||
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||
});
|
||||
|
||||
it('serialize with background', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
|
||||
const lines = [
|
||||
`1\u001b[44m${CLEAR_RIGHT(5)}`,
|
||||
`2${CLEAR_RIGHT(9)}`
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('cause the BCE on scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
|
||||
const padLines = newArray<string>(
|
||||
(index: number) => digitsString(10, index),
|
||||
10
|
||||
);
|
||||
|
||||
const lines = [
|
||||
...padLines,
|
||||
`\u001b[44m${CLEAR_RIGHT(5)}1111111111111111`
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('handle invalid wrap before scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||
|
||||
// A line wrap happened after current line.
|
||||
// But there is no content.
|
||||
// so wrap shouldn't even be able to happen.
|
||||
const segments = [
|
||||
`123456789012345`,
|
||||
MOVE_UP(1),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_DOWN(1),
|
||||
MOVE_LEFT(5),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_UP(1),
|
||||
'1'
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, segments.join(''));
|
||||
});
|
||||
|
||||
it('handle invalid wrap after scroll', async () => {
|
||||
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||
|
||||
const padLines = newArray<string>(
|
||||
(index: number) => digitsString(10, index),
|
||||
10
|
||||
);
|
||||
|
||||
// A line wrap happened after current line.
|
||||
// But there is no content.
|
||||
// so wrap shouldn't even be able to happen.
|
||||
const lines = [
|
||||
padLines.join('\r\n'),
|
||||
'\r\n',
|
||||
`123456789012345`,
|
||||
MOVE_UP(1),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_DOWN(1),
|
||||
MOVE_LEFT(5),
|
||||
CLEAR_RIGHT(5),
|
||||
MOVE_UP(1),
|
||||
'1'
|
||||
];
|
||||
|
||||
await testNormalScreenEqual(page, lines.join(''));
|
||||
});
|
||||
|
||||
describe('handle modes', () => {
|
||||
it('applicationCursorKeysMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?1h', 'test\u001b[?1h');
|
||||
await testSerializeEquals('\u001b[?1l', 'test');
|
||||
});
|
||||
it('applicationKeypadMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?66h', 'test\u001b[?66h');
|
||||
await testSerializeEquals('\u001b[?66l', 'test');
|
||||
});
|
||||
it('bracketedPasteMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?2004h', 'test\u001b[?2004h');
|
||||
await testSerializeEquals('\u001b[?2004l', 'test');
|
||||
});
|
||||
it('insertMode', async () => {
|
||||
await testSerializeEquals('test\u001b[4h', 'test\u001b[4h');
|
||||
await testSerializeEquals('\u001b[4l', 'test');
|
||||
});
|
||||
it('mouseTrackingMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?9h', 'test\u001b[?9h');
|
||||
await testSerializeEquals('\u001b[?9l', 'test');
|
||||
await testSerializeEquals('\u001b[?1000h', 'test\u001b[?1000h');
|
||||
await testSerializeEquals('\u001b[?1000l', 'test');
|
||||
await testSerializeEquals('\u001b[?1002h', 'test\u001b[?1002h');
|
||||
await testSerializeEquals('\u001b[?1002l', 'test');
|
||||
await testSerializeEquals('\u001b[?1003h', 'test\u001b[?1003h');
|
||||
await testSerializeEquals('\u001b[?1003l', 'test');
|
||||
});
|
||||
it('originMode', async () => {
|
||||
// origin mode moves cursor to (0,0)
|
||||
await testSerializeEquals('test\u001b[?6h', 'test\u001b[4D\u001b[?6h');
|
||||
await testSerializeEquals('\u001b[?6l', 'test\u001b[4D');
|
||||
});
|
||||
it('reverseWraparoundMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?45h', 'test\u001b[?45h');
|
||||
await testSerializeEquals('\u001b[?45l', 'test');
|
||||
});
|
||||
it('sendFocusMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?1004h', 'test\u001b[?1004h');
|
||||
await testSerializeEquals('\u001b[?1004l', 'test');
|
||||
});
|
||||
it('wraparoundMode', async () => {
|
||||
await testSerializeEquals('test\u001b[?7l', 'test\u001b[?7l');
|
||||
await testSerializeEquals('\u001b[?7h', 'test');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
||||
const array: T[] = new Array<T>(count);
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
if (typeof initial === 'function') {
|
||||
array[i] = (initial as (index: number) => T)(i);
|
||||
} else {
|
||||
array[i] = initial as T;
|
||||
}
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
function digitsString(length: number, from: number = 0, sgr: string = ''): string {
|
||||
let s = sgr;
|
||||
for (let i = 0; i < length; i++) {
|
||||
s += `${(from++) % 10}`;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
const NORMAL = '0';
|
||||
|
||||
const FG_P16_RED = '31';
|
||||
const FG_P16_GREEN = '32';
|
||||
const FG_P16_YELLOW = '33';
|
||||
const FG_P256_RED = '38;5;196';
|
||||
const FG_P256_GREEN = '38;5;46';
|
||||
const FG_P256_YELLOW = '38;5;226';
|
||||
const FG_RGB_RED = '38;2;255;0;0';
|
||||
const FG_RGB_GREEN = '38;2;0;255;0';
|
||||
const FG_RGB_YELLOW = '38;2;255;255;0';
|
||||
const FG_RESET = '39';
|
||||
|
||||
|
||||
const BG_P16_RED = '41';
|
||||
const BG_P16_GREEN = '42';
|
||||
const BG_P16_YELLOW = '43';
|
||||
const BG_P256_RED = '48;5;196';
|
||||
const BG_P256_GREEN = '48;5;46';
|
||||
const BG_P256_YELLOW = '48;5;226';
|
||||
const BG_RGB_RED = '48;2;255;0;0';
|
||||
const BG_RGB_GREEN = '48;2;0;255;0';
|
||||
const BG_RGB_YELLOW = '48;2;255;255;0';
|
||||
const BG_RESET = '49';
|
||||
|
||||
const BOLD = '1';
|
||||
const DIM = '2';
|
||||
const ITALIC = '3';
|
||||
const UNDERLINED = '4';
|
||||
const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
const STRIKETHROUGH = '9';
|
||||
|
||||
const NO_BOLD = '22';
|
||||
const NO_DIM = '22';
|
||||
const NO_ITALIC = '23';
|
||||
const NO_UNDERLINED = '24';
|
||||
const NO_BLINK = '25';
|
||||
const NO_INVERSE = '27';
|
||||
const NO_INVISIBLE = '28';
|
||||
const NO_STRIKETHROUGH = '29';
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" },
|
||||
{ "path": "./benchmark" }
|
||||
]
|
||||
}
|
||||
+60
-10
@@ -3,36 +3,86 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-serialize' {
|
||||
/**
|
||||
* An xterm.js addon that enables web links.
|
||||
* An xterm.js addon that enables serialization of terminal contents.
|
||||
*/
|
||||
export class SerializeAddon implements ITerminalAddon {
|
||||
|
||||
constructor();
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* Activates the addon.
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Serializes terminal rows into a string that can be written back to the terminal
|
||||
* to restore the state. The cursor will also be positioned to the correct cell.
|
||||
* When restoring a terminal it is best to do before `Terminal.open` is called
|
||||
* to avoid wasting CPU cycles rendering incomplete frames.
|
||||
* @param rows The number of rows to serialize, starting from the bottom of the
|
||||
* terminal. This defaults to the number of rows in the viewport.
|
||||
* Serializes terminal rows into a string that can be written back to the terminal to restore
|
||||
* the state. The cursor will also be positioned to the correct cell. When restoring a terminal
|
||||
* it is best to do before `Terminal.open` is called to avoid wasting CPU cycles rendering
|
||||
* incomplete frames.
|
||||
*
|
||||
* It's recommended that you write the serialized data into a terminal of the same size in which
|
||||
* it originated from and then resize it after if needed.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serialize(rows?: number): string;
|
||||
public serialize(options?: ISerializeOptions): string;
|
||||
|
||||
/**
|
||||
* Serializes terminal content as HTML, which can be written to the clipboard using the
|
||||
* `text/html` mimetype. For applications that support it, the pasted text should then retain
|
||||
* its colors/styles.
|
||||
*
|
||||
* @param options Custom options to allow control over what gets serialized.
|
||||
*/
|
||||
public serializeAsHTML(options?: Partial<IHTMLSerializeOptions>): string;
|
||||
|
||||
/**
|
||||
* Disposes the addon.
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
|
||||
export interface ISerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized.
|
||||
*/
|
||||
scrollback?: number;
|
||||
|
||||
/**
|
||||
* Whether to exclude the terminal modes from the serialization. False by default.
|
||||
*/
|
||||
excludeModes?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to exclude the alt buffer from the serialization. False by default.
|
||||
*/
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
export interface IHTMLSerializeOptions {
|
||||
/**
|
||||
* The number of rows in the scrollback buffer to serialize, starting from the bottom of the
|
||||
* scrollback buffer. When not specified, all available rows in the scrollback buffer will be
|
||||
* serialized. This setting is ignored if {@link IHTMLSerializeOptions.onlySelection} is true.
|
||||
*/
|
||||
scrollback: number;
|
||||
|
||||
/**
|
||||
* Whether to only serialize the selection. If false, the whole active buffer is serialized in HTML.
|
||||
* False by default.
|
||||
*/
|
||||
onlySelection: boolean;
|
||||
|
||||
/**
|
||||
* Whether to include the global background of the terminal. False by default.
|
||||
*/
|
||||
includeGlobalBackground: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@ module.exports = {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-unicode11",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,8 +9,13 @@
|
||||
"types": "typings/xterm-addon-unicode11.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { UnicodeV11 } from './UnicodeV11';
|
||||
|
||||
|
||||
export class Unicode11Addon implements ITerminalAddon {
|
||||
public activate(terminal: Terminal): void {
|
||||
terminal.unicode.register(new UnicodeV11());
|
||||
|
||||
@@ -18,8 +18,7 @@
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+4
-7
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -16,10 +16,7 @@ const height = 600;
|
||||
|
||||
describe('Unicode11Addon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
]
|
||||
},
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -32,7 +32,8 @@ module.exports = {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
libraryTarget: 'umd',
|
||||
globalObject: 'this'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-web-links",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,8 +9,13 @@
|
||||
"types": "typings/xterm-addon-web-links.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -3,25 +3,46 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkProvider, IBufferCellPosition, ILink, Terminal } from 'xterm';
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||
|
||||
export interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
|
||||
export class WebLinkProvider implements ILinkProvider {
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _regex: RegExp,
|
||||
private readonly _handler: (event: MouseEvent, uri: string) => void
|
||||
private readonly _handler: (event: MouseEvent, uri: string) => void,
|
||||
private readonly _options: ILinkProviderOptions = {}
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
||||
callback(LinkComputer.computeLink(y, this._regex, this._terminal, this._handler));
|
||||
const links = LinkComputer.computeLink(y, this._regex, this._terminal, this._handler);
|
||||
callback(this._addCallbacks(links));
|
||||
}
|
||||
|
||||
private _addCallbacks(links: ILink[]): ILink[] {
|
||||
return links.map(link => {
|
||||
link.leave = this._options.leave;
|
||||
link.hover = (event: MouseEvent, uri: string): void => {
|
||||
if (this._options.hover) {
|
||||
const { range } = link;
|
||||
this._options.hover(event, uri, range);
|
||||
}
|
||||
};
|
||||
return link;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class LinkComputer {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, handler: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
||||
|
||||
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
|
||||
@@ -58,10 +79,17 @@ export class LinkComputer {
|
||||
endY++;
|
||||
}
|
||||
|
||||
let startX = stringIndex + 1;
|
||||
let startY = startLineIndex + 1;
|
||||
while (startX > terminal.cols) {
|
||||
startX -= terminal.cols;
|
||||
startY++;
|
||||
}
|
||||
|
||||
const range = {
|
||||
start: {
|
||||
x: stringIndex + 1,
|
||||
y: startLineIndex + 1
|
||||
x: startX,
|
||||
y: startY
|
||||
},
|
||||
end: {
|
||||
x: endX,
|
||||
@@ -69,7 +97,7 @@ export class LinkComputer {
|
||||
}
|
||||
};
|
||||
|
||||
result.push({ range, text, activate: handler });
|
||||
result.push({ range, text, activate });
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { WebLinkProvider } from './WebLinkProvider';
|
||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,6})';
|
||||
const tldClause = '([a-z\\.]{2,18})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
@@ -29,7 +29,11 @@ const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
function handleLink(event: MouseEvent, uri: string): void {
|
||||
const newWindow = window.open();
|
||||
if (newWindow) {
|
||||
newWindow.opener = null;
|
||||
try {
|
||||
newWindow.opener = null;
|
||||
} catch {
|
||||
// no-op, Electron can throw
|
||||
}
|
||||
newWindow.location.href = uri;
|
||||
} else {
|
||||
console.warn('Opening link blocked as opener could not be cleared');
|
||||
@@ -43,20 +47,23 @@ export class WebLinksAddon implements ITerminalAddon {
|
||||
|
||||
constructor(
|
||||
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
|
||||
private _options: ILinkMatcherOptions = {},
|
||||
private _options: ILinkMatcherOptions | ILinkProviderOptions = {},
|
||||
private _useLinkProvider: boolean = false
|
||||
) {
|
||||
this._options.matchIndex = 1;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
|
||||
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
|
||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler));
|
||||
const options = this._options as ILinkProviderOptions;
|
||||
const regex = options.urlRegex || strictUrlRegex;
|
||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
||||
} else {
|
||||
// TODO: This should be removed eventually
|
||||
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, this._options);
|
||||
const options = this._options as ILinkMatcherOptions;
|
||||
options.matchIndex = 1;
|
||||
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,7 @@
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+4
-7
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
import { openTerminal, pollFor, writeSync, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -16,10 +16,7 @@ const height = 600;
|
||||
|
||||
describe('WebLinksAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-web-links' {
|
||||
/**
|
||||
@@ -20,7 +20,7 @@ declare module 'xterm-addon-web-links' {
|
||||
* link provider (new) may cause issues. Link provider will eventually be
|
||||
* the default and only option.
|
||||
*/
|
||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions, useLinkProvider?: boolean);
|
||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
@@ -33,4 +33,26 @@ declare module 'xterm-addon-web-links' {
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An object containing options for a link provider.
|
||||
*/
|
||||
export interface ILinkProviderOptions {
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link for a period of
|
||||
* time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}).
|
||||
*/
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
|
||||
/**
|
||||
* A callback that fires when the mouse leaves a link. Note that this can
|
||||
* happen even when tooltipCallback hasn't fired for the link yet.
|
||||
*/
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
|
||||
/**
|
||||
* A callback to use instead of the default one.
|
||||
*/
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
## xterm-addon-webgl
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL-based renderer. This addon requires xterm.js v4+.
|
||||
|
||||
⚠️ This is an experimental addon that is [missing some features and may be unstable](https://github.com/xtermjs/xterm.js/issues?q=is%3Aopen+is%3Aissue+label%3Aarea%2Faddon%2Fwebgl) ⚠️
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-based renderer. This addon requires xterm.js v4+.
|
||||
|
||||
### Install
|
||||
|
||||
@@ -21,3 +19,18 @@ terminal.loadAddon(new WebglAddon());
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage.
|
||||
|
||||
### Handling Context Loss
|
||||
|
||||
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
||||
|
||||
```ts
|
||||
const terminal = new Terminal();
|
||||
const addon = new WebglAddon();
|
||||
addon.onContextLoss(e => {
|
||||
addon.dispose();
|
||||
});
|
||||
terminal.loadAddon(addon);
|
||||
```
|
||||
|
||||
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.9.0",
|
||||
"version": "0.12.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -9,8 +9,14 @@
|
||||
"types": "typings/xterm-addon-webgl.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"webgl",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p src",
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
|
||||
@@ -6,14 +6,11 @@
|
||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { slice } from './TypedArray';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IBufferLine } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
@@ -24,7 +21,6 @@ interface IVertices {
|
||||
* working on the next frame.
|
||||
*/
|
||||
attributesBuffers: Float32Array[];
|
||||
selectionAttributes: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -91,8 +87,7 @@ export class GlyphRenderer {
|
||||
attributesBuffers: [
|
||||
new Float32Array(0),
|
||||
new Float32Array(0)
|
||||
],
|
||||
selectionAttributes: new Float32Array(0)
|
||||
]
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -176,8 +171,9 @@ export class GlyphRenderer {
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null/space character
|
||||
if (code === NULL_CELL_CODE || code === WHITESPACE_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
// Exit early if this is a null character, allow space character to continue as it may have
|
||||
// underline/strikethrough styles
|
||||
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
@@ -186,6 +182,8 @@ export class GlyphRenderer {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the glyph
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
|
||||
} else {
|
||||
@@ -213,121 +211,41 @@ export class GlyphRenderer {
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
public updateSelection(model: IRenderModel): void {
|
||||
public clear(force?: boolean): void {
|
||||
const terminal = this._terminal;
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
|
||||
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
||||
// Don't clear if not forced and the array length is correct
|
||||
if (!force && this._vertices.count === newCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
|
||||
|
||||
if (model.selection.columnSelectMode) {
|
||||
const startCol = model.selection.startCol;
|
||||
const width = model.selection.endCol - startCol;
|
||||
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
||||
for (let y = model.selection.viewportCappedStartRow; y < model.selection.viewportCappedStartRow + height; y++) {
|
||||
this._updateSelectionRange(startCol, startCol + width, y, model, bg);
|
||||
}
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.selection.viewportStartRow === model.selection.viewportCappedStartRow ? model.selection.startCol : 0;
|
||||
const startRowEndCol = model.selection.viewportCappedStartRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(startCol, startRowEndCol, model.selection.viewportCappedStartRow, model, bg);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow - 1, 0);
|
||||
for (let y = model.selection.viewportCappedStartRow + 1; y <= model.selection.viewportCappedStartRow + middleRowsCount; y++) {
|
||||
this._updateSelectionRange(0, startRowEndCol, y, model, bg);
|
||||
}
|
||||
|
||||
// Draw final row
|
||||
if (model.selection.viewportCappedStartRow !== model.selection.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.selection.viewportEndRow === model.selection.viewportCappedEndRow ? model.selection.endCol : terminal.cols;
|
||||
this._updateSelectionRange(0, endCol, model.selection.viewportCappedEndRow, model, bg);
|
||||
// Clear vertices
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
|
||||
const terminal = this._terminal;
|
||||
const row = y + terminal.buffer.active.viewportY;
|
||||
let line: IBufferLine | undefined;
|
||||
for (let x = startCol; x < endCol; x++) {
|
||||
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
const code = model.cells[offset];
|
||||
let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET];
|
||||
if (fg & FgFlags.INVERSE) {
|
||||
const workCell = new AttributeData();
|
||||
workCell.fg = fg;
|
||||
workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET];
|
||||
// Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors
|
||||
// from bg. This is needed since the inverse fg color should be based on the original bg
|
||||
// color, not on the selection color
|
||||
fg = (fg & ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE));
|
||||
switch (workCell.getBgColorMode()) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba;
|
||||
fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(workCell.getBgColor());
|
||||
fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
const c2 = this._colors.background.rgba;
|
||||
fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK;
|
||||
}
|
||||
fg |= Attributes.CM_RGB;
|
||||
}
|
||||
if (code & COMBINED_CHAR_BIT_MASK) {
|
||||
if (!line) {
|
||||
line = terminal.buffer.active.getLine(row);
|
||||
}
|
||||
const chars = line!.getCell(x)!.getChars();
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars);
|
||||
} else {
|
||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx >= this._colors.ansi.length) {
|
||||
throw new Error('No color found for idx ' + idx);
|
||||
}
|
||||
return this._colors.ansi[idx];
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
const terminal = this._terminal;
|
||||
const gl = this._gl;
|
||||
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Update vertices
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
}
|
||||
|
||||
public render(renderModel: IRenderModel, isSelectionVisible: boolean): void {
|
||||
public render(renderModel: IRenderModel): void {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
@@ -351,7 +269,7 @@ export class GlyphRenderer {
|
||||
let bufferLength = 0;
|
||||
for (let y = 0; y < renderModel.lineLengths.length; y++) {
|
||||
const si = y * this._terminal.cols * INDICES_PER_CELL;
|
||||
const sub = (isSelectionVisible ? this._vertices.selectionAttributes : this._vertices.attributes).subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
const sub = this._vertices.attributes.subarray(si, si + renderModel.lineLengths[y] * INDICES_PER_CELL);
|
||||
activeBuffer.set(sub, bufferLength);
|
||||
bufferLength += sub.length;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
|
||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
|
||||
@@ -49,7 +49,6 @@ void main() {
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
selection: Float32Array;
|
||||
count: number;
|
||||
}
|
||||
|
||||
@@ -66,12 +65,10 @@ export class RectangleRenderer {
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat!: Float32Array;
|
||||
private _selectionFloat!: Float32Array;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY),
|
||||
selection: new Float32Array(3 * INDICES_PER_RECTANGLE)
|
||||
attributes: new Float32Array(INITIAL_BUFFER_RECTANGLE_CAPACITY)
|
||||
};
|
||||
|
||||
constructor(
|
||||
@@ -137,11 +134,6 @@ export class RectangleRenderer {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
|
||||
// Bind selection buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.selection, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, 3);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
@@ -155,7 +147,6 @@ export class RectangleRenderer {
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
this._selectionFloat = this._colorToFloat32Array(this._colors.selectionOpaque);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
@@ -171,73 +162,6 @@ export class RectangleRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
public updateSelection(model: ISelectionRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
if (!model.hasSelection) {
|
||||
fill(this._vertices.selection, 0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (model.columnSelectMode) {
|
||||
const startCol = model.startCol;
|
||||
const width = model.endCol - startCol;
|
||||
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
width * this._dimensions.scaledCellWidth,
|
||||
height * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
||||
const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
0,
|
||||
startCol * this._dimensions.scaledCellWidth,
|
||||
model.viewportCappedStartRow * this._dimensions.scaledCellHeight,
|
||||
(startRowEndCol - startCol) * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(model.viewportCappedEndRow - model.viewportCappedStartRow - 1, 0);
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE,
|
||||
0,
|
||||
(model.viewportCappedStartRow + 1) * this._dimensions.scaledCellHeight,
|
||||
terminal.cols * this._dimensions.scaledCellWidth,
|
||||
middleRowsCount * this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
|
||||
// Draw final row
|
||||
if (model.viewportCappedStartRow !== model.viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = model.viewportEndRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
||||
this._addRectangleFloat(
|
||||
this._vertices.selection,
|
||||
INDICES_PER_RECTANGLE * 2,
|
||||
0,
|
||||
model.viewportCappedEndRow * this._dimensions.scaledCellHeight,
|
||||
endCol * this._dimensions.scaledCellWidth,
|
||||
this._dimensions.scaledCellHeight,
|
||||
this._selectionFloat
|
||||
);
|
||||
} else {
|
||||
fill(this._vertices.selection, 0, INDICES_PER_RECTANGLE * 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public updateBackgrounds(model: IRenderModel): void {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
@@ -3,14 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
@@ -20,10 +25,16 @@ export class WebglAddon implements ITerminalAddon {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate WebglAddon before Terminal.open');
|
||||
}
|
||||
if (isSafari) {
|
||||
throw new Error('Webgl is not currently supported on Safari');
|
||||
}
|
||||
this._terminal = terminal;
|
||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer);
|
||||
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
|
||||
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,18 @@ import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -27,6 +32,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _workCell: CellData = new CellData();
|
||||
private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 };
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
@@ -41,9 +47,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -52,7 +63,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
|
||||
new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._onRequestRedraw)
|
||||
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
@@ -82,6 +93,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
if (!this._gl) {
|
||||
throw new Error('WebGL2 not supported ' + this._gl);
|
||||
}
|
||||
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
@@ -94,8 +108,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
this._core.screenElement!.removeChild(this._canvas);
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._canvas.parentElement?.removeChild(this._canvas);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -106,22 +122,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
// Clear layers and force a full render
|
||||
this._renderLayers.forEach(l => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
});
|
||||
}
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
this._glyphRenderer.setColors();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
this._glyphRenderer.updateSelection(this._model);
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
this._model.clearSelection();
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
@@ -138,10 +150,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._updateDimensions();
|
||||
|
||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||
this._rectangleRenderer.onResize();
|
||||
|
||||
// Resize all render layers
|
||||
this._renderLayers.forEach(l => l.resize(this._terminal, this.dimensions));
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this._terminal, this.dimensions);
|
||||
}
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
@@ -152,6 +165,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// Resize the screen
|
||||
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
|
||||
this._rectangleRenderer.onResize();
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
@@ -159,7 +174,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
this._model.clearSelection();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
@@ -167,30 +181,35 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onBlur(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._renderLayers.forEach(l => l.onFocus(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onFocus(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._renderLayers.forEach(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||
|
||||
for (const l of this._renderLayers) {
|
||||
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
this._updateSelectionModel(start, end, columnSelectMode);
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
this._glyphRenderer.updateSelection(this._model);
|
||||
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onCursorMove(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onOptionsChanged(this._terminal);
|
||||
}
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
@@ -207,7 +226,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
return;
|
||||
}
|
||||
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
||||
if (!('getRasterizedGlyph' in atlas)) {
|
||||
throw new Error('The webgl renderer only works with the webgl char atlas');
|
||||
}
|
||||
@@ -220,12 +239,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._charAtlas?.clearTexture();
|
||||
this._model.clear();
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
this._glyphRenderer.updateSelection(this._model);
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
this._model.clear();
|
||||
this._glyphRenderer.clear(true);
|
||||
for (const l of this._renderLayers) {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
@@ -248,12 +270,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Update render layers
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
for (const l of this._renderLayers) {
|
||||
l.onGridChanged(this._terminal, start, end);
|
||||
}
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._model.clear();
|
||||
this._model.clearSelection();
|
||||
this._updateSelectionModel(undefined, undefined);
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
@@ -261,51 +285,190 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.render();
|
||||
this._glyphRenderer.render(this._model, this._model.selection.hasSelection);
|
||||
this._glyphRenderer.render(this._model);
|
||||
}
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
let cell: ICellData = this._workCell;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row)!;
|
||||
this._model.lineLengths[y] = 0;
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
line.loadCell(x, this._workCell);
|
||||
line.loadCell(x, cell);
|
||||
|
||||
const chars = this._workCell.getChars();
|
||||
let code = this._workCell.getCode();
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift()!;
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
cell = new JoinedCellData(
|
||||
cell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
range[1] - range[0]
|
||||
);
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
let code = cell.getCode();
|
||||
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._loadColorsForCell(x, row);
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
}
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Flag combined chars with a bit mask so they're easily identifiable
|
||||
if (chars.length > 1) {
|
||||
code = code | COMBINED_CHAR_BIT_MASK;
|
||||
code |= COMBINED_CHAR_BIT_MASK;
|
||||
}
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
cell = this._workCell;
|
||||
|
||||
// Null out non-first cells
|
||||
for (x++; x < lastCharX; x++) {
|
||||
const j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, NULL_CELL_CHAR);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
/**
|
||||
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
|
||||
* necessary which is why the work cell object is not used.
|
||||
*/
|
||||
private _loadColorsForCell(x: number, y: number): void {
|
||||
this._workColors.bg = this._workCell.bg;
|
||||
this._workColors.fg = this._workCell.fg;
|
||||
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the selection color if needed
|
||||
if (this._isCellSelected(x, y)) {
|
||||
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
|
||||
if (this._colors.selectionForeground) {
|
||||
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if (bgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
if (fgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this._workColors.fg & FgFlags.INVERSE) {
|
||||
if (bgOverride !== undefined && fgOverride === undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
if (bgOverride === undefined && fgOverride !== undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this._workColors.bg = bgOverride ?? this._workColors.bg;
|
||||
this._workColors.fg = fgOverride ?? this._workColors.fg;
|
||||
}
|
||||
|
||||
private _isCellSelected(x: number, y: number): boolean {
|
||||
if (!this._model.selection.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= this._terminal.buffer.active.viewportY;
|
||||
if (this._model.selection.columnSelectMode) {
|
||||
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
||||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
@@ -365,18 +528,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// will be floored because since lineHeight can never be lower then 1, there
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.getOption('lineHeight'));
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.getOption('lineHeight') === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.getOption('letterSpacing'));
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.getOption('letterSpacing') / 2);
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
@@ -407,4 +570,54 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Share impl with core
|
||||
export class JoinedCellData extends AttributeData implements ICellData {
|
||||
private _width: number;
|
||||
// .content carries no meaning for joined CellData, simply nullify it
|
||||
// thus we have to overload all other .content accessors
|
||||
public content: number = 0;
|
||||
public fg: number;
|
||||
public bg: number;
|
||||
public combinedData: string = '';
|
||||
|
||||
constructor(firstCell: ICellData, chars: string, width: number) {
|
||||
super();
|
||||
this.fg = firstCell.fg;
|
||||
this.bg = firstCell.bg;
|
||||
this.combinedData = chars;
|
||||
this._width = width;
|
||||
}
|
||||
|
||||
public isCombined(): number {
|
||||
// always mark joined cell data as combined
|
||||
return Content.IS_COMBINED_MASK;
|
||||
}
|
||||
|
||||
public getWidth(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public getChars(): string {
|
||||
return this.combinedData;
|
||||
}
|
||||
|
||||
public getCode(): number {
|
||||
// code always gets the highest possible fake codepoint (read as -1)
|
||||
// this is needed as code is used by caches as identifier
|
||||
return 0x1FFFFF;
|
||||
}
|
||||
|
||||
public setFromCharData(value: CharData): void {
|
||||
throw new Error('not implemented');
|
||||
}
|
||||
|
||||
public getAsCharData(): CharData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,12 @@ const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
export function acquireCharAtlas(
|
||||
terminal: Terminal,
|
||||
colors: IColorSet,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number
|
||||
): WebglCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
|
||||
@@ -6,14 +6,15 @@
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { Terminal, FontWeight } from 'xterm';
|
||||
import { IColorSet, IColor } from 'browser/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
|
||||
const NULL_COLOR: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
|
||||
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
@@ -22,22 +23,28 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
|
||||
cursorAccent: NULL_COLOR,
|
||||
selectionTransparent: NULL_COLOR,
|
||||
selectionOpaque: NULL_COLOR,
|
||||
selectionForeground: NULL_COLOR,
|
||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||
// dynamic character atlas.
|
||||
ansi: colors.ansi.slice(),
|
||||
contrastCache: colors.contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: terminal.options.customGlyphs!,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
letterSpacing: terminal.options.letterSpacing!,
|
||||
lineHeight: terminal.options.lineHeight!,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.getOption('fontFamily'),
|
||||
fontSize: terminal.getOption('fontSize'),
|
||||
fontWeight: terminal.getOption('fontWeight') as FontWeight,
|
||||
fontWeightBold: terminal.getOption('fontWeightBold') as FontWeight,
|
||||
allowTransparency: terminal.getOption('allowTransparency'),
|
||||
drawBoldTextInBrightColors: terminal.getOption('drawBoldTextInBrightColors'),
|
||||
minimumContrastRatio: terminal.getOption('minimumContrastRatio'),
|
||||
fontFamily: terminal.options.fontFamily!,
|
||||
fontSize: terminal.options.fontSize!,
|
||||
fontWeight: terminal.options.fontWeight as FontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold as FontWeight,
|
||||
allowTransparency: terminal.options.allowTransparency!,
|
||||
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors!,
|
||||
minimumContrastRatio: terminal.options.minimumContrastRatio!,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
@@ -49,6 +56,9 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.customGlyphs === b.customGlyphs &&
|
||||
a.lineHeight === b.lineHeight &&
|
||||
a.letterSpacing === b.letterSpacing &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
|
||||
@@ -17,11 +17,16 @@ export interface IGlyphIdentifier {
|
||||
}
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
customGlyphs: boolean;
|
||||
devicePixelRatio: number;
|
||||
letterSpacing: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
|
||||
@@ -4,17 +4,19 @@
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DIM_OPACITY } from 'browser/renderer/atlas/Constants';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
|
||||
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { channels, rgba } from 'browser/Color';
|
||||
import { channels, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
@@ -80,12 +82,12 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', {alpha: true}));
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCharHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {alpha: this._config.allowTransparency}));
|
||||
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -215,8 +217,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
|
||||
if (minimumContrastCss) {
|
||||
return minimumContrastCss;
|
||||
}
|
||||
@@ -237,7 +239,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
const bg = this._config.colors.background.css;
|
||||
if (bg.length === 9) {
|
||||
// Remove bg alpha channel if present
|
||||
return bg.substr(0, 7);
|
||||
return bg.slice(0, 7);
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
@@ -280,8 +282,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1) {
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -317,6 +319,18 @@ export class WebglCharAtlas implements IDisposable {
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
@@ -331,6 +345,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
const inverse = !!this._workAttributeData.isInverse();
|
||||
const dim = !!this._workAttributeData.isDim();
|
||||
const italic = !!this._workAttributeData.isItalic();
|
||||
const underline = !!this._workAttributeData.isUnderline();
|
||||
const strikethrough = !!this._workAttributeData.isStrikethrough();
|
||||
let fgColor = this._workAttributeData.getFgColor();
|
||||
let fgColorMode = this._workAttributeData.getFgColorMode();
|
||||
let bgColor = this._workAttributeData.getBgColor();
|
||||
@@ -358,17 +374,65 @@ export class WebglCharAtlas implements IDisposable {
|
||||
const fontStyle = italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = 'middle';
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING + this._config.scaledCharHeight / 2);
|
||||
if (!drawSuccess) {
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
}
|
||||
|
||||
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible,
|
||||
// try for a maximum of 5 pixels.
|
||||
if (chars === '_' && !this._config.allowTransparency) {
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
|
||||
if (isBeyondCellBounds) {
|
||||
for (let offset = 1; offset <= 5; offset++) {
|
||||
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
|
||||
if (!isBeyondCellBounds) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw underline and strikethrough
|
||||
if (underline || strikethrough) {
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10));
|
||||
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
this._tmpCtx.beginPath();
|
||||
if (underline) {
|
||||
this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset);
|
||||
}
|
||||
if (strikethrough) {
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.closePath();
|
||||
}
|
||||
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
@@ -391,11 +455,11 @@ export class WebglCharAtlas implements IDisposable {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox);
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
if (this._currentRowX + this._config.scaledCharWidth > TEXTURE_WIDTH) {
|
||||
if (this._currentRowX + rasterizedGlyph.size.x > TEXTURE_WIDTH) {
|
||||
this._currentRowX = 0;
|
||||
this._currentRowY += this._currentRowHeight;
|
||||
this._currentRowHeight = 0;
|
||||
@@ -424,11 +488,13 @@ export class WebglCharAtlas implements IDisposable {
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph {
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
@@ -442,8 +508,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
@@ -455,10 +521,10 @@ export class WebglCharAtlas implements IDisposable {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.right = this._tmpCanvas.width;
|
||||
boundingBox.right = width;
|
||||
found = false;
|
||||
for (let x = this._tmpCanvas.width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
@@ -470,10 +536,10 @@ export class WebglCharAtlas implements IDisposable {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.bottom = this._tmpCanvas.height;
|
||||
boundingBox.bottom = height;
|
||||
found = false;
|
||||
for (let y = this._tmpCanvas.height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
@@ -497,8 +563,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING,
|
||||
y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING
|
||||
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0),
|
||||
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { IRenderLayer } from './Types';
|
||||
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
|
||||
@@ -46,7 +47,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', {alpha: this._alpha}));
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
@@ -92,7 +93,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
@@ -224,12 +225,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,10 +254,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: Terminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.getOption('fontWeightBold') : terminal.getOption('fontWeight');
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${terminal.getOption('fontSize') * window.devicePixelRatio}px ${terminal.getOption('fontFamily')}`;
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
|
||||
@@ -31,9 +31,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
private readonly _terminal: ITerminal,
|
||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
@@ -49,7 +51,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
// TODO: Consider initial options? Maybe onOptionsChanged should be called at the end of open?
|
||||
this.onOptionsChanged(terminal);
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -66,29 +68,22 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
public reset(terminal: Terminal): void {
|
||||
this._clearCursor();
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this.onOptionsChanged(terminal);
|
||||
}
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
this.onOptionsChanged(terminal);
|
||||
}
|
||||
|
||||
public onBlur(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.pause();
|
||||
}
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onFocus(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.resume(terminal);
|
||||
} else {
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
this._cursorBlinkStateManager?.resume(terminal);
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {
|
||||
if (terminal.getOption('cursorBlink')) {
|
||||
if (terminal.options.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
|
||||
this._render(terminal, true);
|
||||
@@ -104,9 +99,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onCursorMove(terminal: Terminal): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation(terminal);
|
||||
}
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
@@ -120,7 +113,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
|
||||
// Don't draw the cursor if it's hidden
|
||||
// TODO: Need to expose API for this
|
||||
if (!(terminal as any)._core._coreService.isCursorInitialized || (terminal as any)._core._coreService.isCursorHidden) {
|
||||
if (!this._terminal.coreService.isCursorInitialized || this._terminal.coreService.isCursorHidden) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
@@ -147,7 +140,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
const cursorStyle = terminal.getOption('cursorStyle');
|
||||
const cursorStyle = terminal.options.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
@@ -157,7 +150,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = cursorStyle;
|
||||
this._state.style = cursorStyle!;
|
||||
this._state.width = this._cell.getWidth();
|
||||
return;
|
||||
}
|
||||
@@ -173,7 +166,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (this._state.x === cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === isTerminalFocused(terminal) &&
|
||||
this._state.style === terminal.getOption('cursorStyle') &&
|
||||
this._state.style === terminal.options.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
@@ -181,19 +174,24 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._cursorRenderers[terminal.options.cursorStyle || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = terminal.getOption('cursorStyle');
|
||||
this._state.style = terminal.options.cursorStyle!;
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
// Avoid potential rounding errors when device pixel ratio is less than 1
|
||||
if (window.devicePixelRatio < 1) {
|
||||
this._clearAll();
|
||||
} else {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
}
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -207,7 +205,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth'));
|
||||
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
@@ -296,6 +294,7 @@ class CursorBlinkStateManager {
|
||||
// Clear any existing interval
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
|
||||
// Setup the initial timeout which will hide the cursor, this is done before
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../out-test/api/TestUtils"
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
|
||||
+182
-61
@@ -3,13 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalOptions } from '../../../src/common/Types';
|
||||
import { ITheme } from 'xterm';
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright-core';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { ITheme } from 'xterm';
|
||||
import { getBrowserType, launchBrowser, openTerminal, pollFor, timeout, writeSync } from '../../../out-test/api/TestUtils';
|
||||
import { ITerminalOptions } from '../../../src/common/Types';
|
||||
|
||||
const APP = 'http://127.0.0.1:3000/test';
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
@@ -18,7 +18,7 @@ const height = 600;
|
||||
|
||||
describe('WebGL Renderer Integration Tests', async () => {
|
||||
const browserType = getBrowserType();
|
||||
const isHeadless = process.argv.indexOf('--headless') !== -1;
|
||||
const isHeadless = process.argv.includes('--headless');
|
||||
// Firefox works only in non-headless mode https://github.com/microsoft/playwright/issues/1032
|
||||
const areTestsEnabled = browserType.name() === 'chromium' || (browserType.name() === 'firefox' && !isHeadless);
|
||||
const itWebgl = areTestsEnabled ? it : it.skip;
|
||||
@@ -49,7 +49,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -73,8 +73,8 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('drawBoldTextInBrightColors', true);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.drawBoldTextInBrightColors = true;
|
||||
`);
|
||||
await writeSync(page, `\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
@@ -98,7 +98,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -121,7 +121,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -144,7 +144,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -167,7 +167,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]);
|
||||
@@ -190,7 +190,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
cyan: '#131415',
|
||||
white: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -213,7 +213,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -236,7 +236,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightCyan: '#131415',
|
||||
brightWhite: '#161718'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, `\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||
@@ -260,9 +260,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -280,9 +280,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -300,9 +300,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -320,9 +320,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -356,9 +356,9 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
@@ -715,12 +715,13 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#eeeeec'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('minimumContrastRatio', 1);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
@@ -742,21 +743,21 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
// Setting and check for minimum contrast values, note that these are note
|
||||
// exact to the contrast ratio, if the increase luminance algorithm
|
||||
// changes then these will probably fail
|
||||
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
|
||||
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [176, 180, 180, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [238, 158, 158, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [197, 223, 171, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [235, 221, 158, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [124, 156, 198, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [183, 165, 187, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [152, 198, 110, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [208, 179, 49, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [161, 183, 215, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [191, 174, 194, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 1), [110, 197, 198, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 1), [211, 215, 207, 255]);
|
||||
await pollFor(page, () => getCellColor(1, 2), [183, 185, 183, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 2), [249, 156, 156, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [138, 226, 52, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [252, 233, 79, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [114, 159, 207, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [190, 152, 185, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [154, 186, 221, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [203, 173, 199, 255]);
|
||||
// Unchanged
|
||||
await pollFor(page, () => getCellColor(7, 2), [0x34, 0xe2, 0xe2, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
|
||||
@@ -783,12 +784,13 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
brightWhite: '#eeeeec'
|
||||
};
|
||||
await page.evaluate(`
|
||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||
window.term.setOption('minimumContrastRatio', 1);
|
||||
window.term.options.theme = ${JSON.stringify(theme)};
|
||||
window.term.options.minimumContrastRatio = 1;
|
||||
`);
|
||||
// Block characters ignore block elements so a different char is used here
|
||||
await writeSync(page,
|
||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||
`\\x1b[30m■\\x1b[31m■\\x1b[32m■\\x1b[33m■\\x1b[34m■\\x1b[35m■\\x1b[36m■\\x1b[37m■\\r\\n` +
|
||||
`\\x1b[90m■\\x1b[91m■\\x1b[92m■\\x1b[93m■\\x1b[94m■\\x1b[95m■\\x1b[96m■\\x1b[97m■`
|
||||
);
|
||||
// Validate before minimumContrastRatio is applied
|
||||
await pollFor(page, () => getCellColor(1, 1), [0x2e, 0x34, 0x36, 255]);
|
||||
@@ -810,21 +812,21 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
// Setting and check for minimum contrast values, note that these are note
|
||||
// exact to the contrast ratio, if the increase luminance algorithm
|
||||
// changes then these will probably fail
|
||||
await page.evaluate(`window.term.setOption('minimumContrastRatio', 10);`);
|
||||
await page.evaluate(`window.term.options.minimumContrastRatio = 10;`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [46, 52, 54, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [132, 0, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [78, 154, 6, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [114, 93, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [19, 40, 68, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [60, 40, 64, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 1), [36, 72, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 1), [72, 59, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 1), [32, 64, 106, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 1), [75, 51, 80, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 1), [0, 71, 72, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 1), [64, 64, 63, 255]);
|
||||
await pollFor(page, () => getCellColor(1, 2), [61, 63, 59, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 2), [125, 19, 19, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [89, 146, 32, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [105, 98, 32, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [36, 52, 70, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [64, 45, 63, 255]);
|
||||
await pollFor(page, () => getCellColor(3, 2), [40, 67, 13, 255]);
|
||||
await pollFor(page, () => getCellColor(4, 2), [67, 63, 19, 255]);
|
||||
await pollFor(page, () => getCellColor(5, 2), [45, 65, 87, 255]);
|
||||
await pollFor(page, () => getCellColor(6, 2), [81, 57, 78, 255]);
|
||||
await pollFor(page, () => getCellColor(7, 2), [13, 67, 67, 255]);
|
||||
await pollFor(page, () => getCellColor(8, 2), [64, 64, 64, 255]);
|
||||
});
|
||||
@@ -843,7 +845,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
background: '#00FF00',
|
||||
selection: '#0000FF'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, ` █\\x1b[7m█\\x1b[0m`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]);
|
||||
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
|
||||
@@ -867,13 +869,121 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
const theme: ITheme = {
|
||||
background: '#ff000080'
|
||||
};
|
||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
// Inverse background should be opaque
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
|
||||
itWebgl('transparent background inverse', async () => {
|
||||
const theme: ITheme = {
|
||||
selectionForeground: '#ff0000'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`window.term.selectAll()`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('decoration color overrides', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
|
||||
itWebgl('foregroundColor', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `█`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
itWebgl('foregroundColor should ignore inverse', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m█\\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
});
|
||||
itWebgl('foregroundColor should ignore inverse (only fg on decoration)', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
width: 2,
|
||||
foregroundColor: '#ff0000'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m█ \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]); // inverse foreground of '█' should be decoration fg override
|
||||
await pollFor(page, () => getCellColor(2, 1), [255, 255, 255, 255]); // inverse background of ' ' should be default foreground
|
||||
});
|
||||
itWebgl('backgroundColor', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = ` `;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
|
||||
});
|
||||
itWebgl('backgroundColor should ignore inverse', async () => {
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
foregroundColor: '#ff0000',
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
const data = `\\x1b[7m \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
|
||||
});
|
||||
itWebgl('backgroundColor should ignore inverse (only bg on decoration)', async () => {
|
||||
const data = `\\x1b[7m█ \\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`
|
||||
const marker = window.term.registerMarker(-window.term.buffer.active.cursorY);
|
||||
window.term.registerDecoration({
|
||||
marker,
|
||||
width: 2,
|
||||
backgroundColor: '#0000ff'
|
||||
});
|
||||
`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]); // inverse foreground of '█' should be default
|
||||
await pollFor(page, () => getCellColor(2, 1), [0, 0, 255, 255]); // inverse background of ' ' should be decoration bg override
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
@@ -890,11 +1000,22 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
}
|
||||
|
||||
async function getCellPixels(col: number, row: number): Promise<number[]> {
|
||||
await page.evaluate(`
|
||||
window.gl = window.term._core._renderService._renderer._gl;
|
||||
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor(${col - 1} * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
|
||||
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
}
|
||||
|
||||
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({ dumpio: true,
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
+6
-1
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-webgl' {
|
||||
/**
|
||||
@@ -29,5 +29,10 @@ declare module 'xterm-addon-webgl' {
|
||||
* Clears the terminal's texture atlas and triggers a redraw.
|
||||
*/
|
||||
public clearTextureAtlas(): void;
|
||||
|
||||
/**
|
||||
* Fired when the WebglRenderer loses context
|
||||
*/
|
||||
public get onContextLoss(): IEvent<void>;
|
||||
}
|
||||
}
|
||||
|
||||
+34
-31
@@ -10,7 +10,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -59,11 +59,11 @@ jobs:
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
vmImage: 'windows-2019'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -92,53 +92,51 @@ jobs:
|
||||
displayName: Install required packages
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
versionSpec: '1.x'
|
||||
displayName: 'Install Yarn'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
key: yarn2 | $(Agent.OS) | yarn.lock
|
||||
path: node_modules
|
||||
displayName: Cache node modules
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn start &
|
||||
sleep 10
|
||||
displayName: 'Start test server'
|
||||
- script: yarn test-api-chromium --headless --forbid-only
|
||||
displayName: 'Integration tests (Chromium)'
|
||||
- script: xvfb-run --auto-servernum -- bash -c "yarn test-api-firefox --headless --forbid-only"
|
||||
displayName: 'Integration tests (Firefox)'
|
||||
|
||||
- job: macOS_IntegrationTests
|
||||
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
|
||||
# - job: macOS_IntegrationTests
|
||||
# pool:
|
||||
# vmImage: 'macOS-10.15'
|
||||
# steps:
|
||||
# - task: NodeTool@0
|
||||
# inputs:
|
||||
# versionSpec: '14.x'
|
||||
# displayName: 'Install Node.js'
|
||||
# - script: yarn --frozen-lockfile
|
||||
# displayName: 'Install dependencies and build'
|
||||
# - script: yarn test-api-chromium --headless --forbid-only
|
||||
# displayName: 'Integration tests (Chromium)'
|
||||
# - script: yarn test-api-firefox --headless --forbid-only
|
||||
# displayName: 'Integration tests (Firefox)'
|
||||
# - script: yarn test-api-webkit --headless --forbid-only
|
||||
# displayName: 'Integration tests (Webkit)'
|
||||
|
||||
- job: Windows_IntegrationTests
|
||||
pool:
|
||||
vmImage: 'macOS-10.15'
|
||||
vmImage: 'windows-2019'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
key: yarn2 | $(Agent.OS) | yarn.lock
|
||||
path: node_modules
|
||||
displayName: Cache node modules
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn start &
|
||||
sleep 10
|
||||
displayName: 'Start test server'
|
||||
- script: yarn test-api-chromium --headless --forbid-only
|
||||
displayName: 'Integration tests (Chromium)'
|
||||
- script: yarn test-api-firefox --headless --forbid-only
|
||||
displayName: 'Integration tests (Firefox)'
|
||||
- script: yarn test-api-webkit --headless --forbid-only
|
||||
displayName: 'Integration tests (Webkit)'
|
||||
|
||||
- job: Release
|
||||
dependsOn:
|
||||
@@ -146,14 +144,15 @@ jobs:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux_IntegrationTests
|
||||
- macOS_IntegrationTests
|
||||
# - macOS_IntegrationTests
|
||||
- Windows_IntegrationTests
|
||||
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
vmImage: 'ubuntu-18.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -166,5 +165,9 @@ jobs:
|
||||
displayName: Cache node modules
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn package-headless
|
||||
node ./bin/package_headless.js
|
||||
displayName: 'Package xterm-headless'
|
||||
- script: NPM_AUTH_TOKEN="$(NPM_AUTH_TOKEN)" node ./bin/publish.js
|
||||
displayName: 'Package and publish to npm'
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const playwright = require('playwright-core');
|
||||
const fs = require('fs');
|
||||
|
||||
// Default to chromium
|
||||
let browserType = playwright['chromium'];
|
||||
const index = process.argv.indexOf('--browser');
|
||||
if (index !== -1 && process.argv.length > index + 1 && typeof process.argv[index + 1] === 'string') {
|
||||
const string = process.argv[index + 1];
|
||||
if (string === 'firefox' || string === 'webkit') {
|
||||
browserType = playwright[string];
|
||||
}
|
||||
}
|
||||
|
||||
const exists = fs.existsSync(browserType.executablePath());
|
||||
if (!exists) {
|
||||
console.log(`Downloading ${browserType.name()}`);
|
||||
browserType.downloadBrowserIfNeeded().then(() => process.exit(0));
|
||||
}
|
||||
@@ -11,13 +11,13 @@ const Mustache = require('mustache');
|
||||
* regexp to fetch all comments
|
||||
* Fetches all multiline comments and single lines containing '// @vt:'.
|
||||
*/
|
||||
const REX_COMMENTS = /^\s*?[/][*][*]([\s\S]*?)[*][/]|^\s*?\/\/ ([@]vt[:].*?)$/mug;
|
||||
const REX_COMMENTS = /^\s*?\/\*\*([\S\s]*?)\*\/|^\s*?\/\/ (@vt:.*?)$/mug;
|
||||
|
||||
/**
|
||||
* regexp to parse the @vt line
|
||||
* expected data - "@vt: <status> <kind> <mnemonic> "<name>" "<sequence>" "<short description>"
|
||||
*/
|
||||
const REX_VT_LINE = /^[@]vt\:\s*(\w+|#\w+|#\w+\[.*?\])\s*(\w+)\s*(\w+)\s*"(.*?)"\s*"(.*?)"\s*"(.*?)".*$/;
|
||||
const REX_VT_LINE = /^@vt:\s*(\w+|#\w+|#\w+\[.*?\])\s*(\w+)\s*(\w+)\s*"(.*?)"\s*"(.*?)"\s*"(.*?)".*$/;
|
||||
|
||||
// known vt command types
|
||||
const TYPES = [
|
||||
@@ -359,10 +359,10 @@ function empty(ar) {
|
||||
}
|
||||
|
||||
function* parseMultiLineGen(filename, s) {
|
||||
if (!~s.indexOf('@vt:')) {
|
||||
if (!s.includes('@vt:')) {
|
||||
return;
|
||||
}
|
||||
const lines = s.split('\n').map(el => el.trim().replace(/[*]/, '').replace(/\s/, ''));
|
||||
const lines = s.split('\n').map(el => el.trim().replace(/\*/, '').replace(/\s/, ''));
|
||||
let grabLine = false;
|
||||
let longDescription = [];
|
||||
let feature = undefined;
|
||||
@@ -413,7 +413,7 @@ function parseSingleLine(filename, s) {
|
||||
const line = s.trim();
|
||||
const match = line.match(REX_VT_LINE);
|
||||
if (match !== null) {
|
||||
if (!~TYPES.indexOf(match[2])) {
|
||||
if (!TYPES.includes(match[2])) {
|
||||
throw new Error(`unkown vt-command type "${match[2]}" specified in "${filename}"`);
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -25,7 +25,7 @@ if (fs.existsSync(addonsPath)) {
|
||||
|
||||
// walk all addon folders
|
||||
fs.readdir(addonsPath, (err, files) => {
|
||||
files.forEach(folder => {
|
||||
for (const folder of files) {
|
||||
const addonPath = path.join(addonsPath, folder);
|
||||
|
||||
// install only if there are dependencies listed
|
||||
@@ -51,6 +51,6 @@ if (fs.existsSync(addonsPath)) {
|
||||
} else {
|
||||
console.log('Skipped', folder);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const { exec } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const { join } = require('path');
|
||||
|
||||
const repoRoot = join(__dirname, '..');
|
||||
const headlessRoot = join(repoRoot, 'headless');
|
||||
|
||||
console.log('> headless/package.json');
|
||||
const xtermPackageJson = require('../package.json');
|
||||
const xtermHeadlessPackageJson = {
|
||||
...xtermPackageJson,
|
||||
name: 'xterm-headless',
|
||||
description: 'A headless terminal component that runs in Node.js',
|
||||
main: 'lib-headless/xterm-headless.js',
|
||||
types: 'typings/xterm-headless.d.ts',
|
||||
};
|
||||
delete xtermHeadlessPackageJson['scripts'];
|
||||
delete xtermHeadlessPackageJson['devDependencies'];
|
||||
delete xtermHeadlessPackageJson['style'];
|
||||
fs.writeFileSync(join(headlessRoot, 'package.json'), JSON.stringify(xtermHeadlessPackageJson, null, 1));
|
||||
console.log(fs.readFileSync(join(headlessRoot, 'package.json')).toString());
|
||||
|
||||
console.log('> headless/typings/');
|
||||
mkdirF(join(headlessRoot, 'typings'));
|
||||
fs.copyFileSync(
|
||||
join(repoRoot, 'typings/xterm-headless.d.ts'),
|
||||
join(headlessRoot, 'typings/xterm-headless.d.ts')
|
||||
);
|
||||
|
||||
console.log('> headless/logo-full.png');
|
||||
fs.copyFileSync(
|
||||
join(repoRoot, 'logo-full.png'),
|
||||
join(headlessRoot, 'logo-full.png')
|
||||
);
|
||||
|
||||
function mkdirF(p) {
|
||||
if (!fs.existsSync(p)) {
|
||||
fs.mkdirSync(p);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('> Publish dry run');
|
||||
exec('npm publish --dry-run', { cwd: headlessRoot }, (error, stdout, stderr) => {
|
||||
if (error) {
|
||||
console.log(`error: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
if (stderr) {
|
||||
console.error(`stderr:\n${stderr}`);
|
||||
}
|
||||
console.log(`stdout:\n${stdout}`);
|
||||
});
|
||||
+10
-9
@@ -11,7 +11,7 @@ const path = require('path');
|
||||
// Setup auth
|
||||
fs.writeFileSync(`${process.env['HOME']}/.npmrc`, `//registry.npmjs.org/:_authToken=${process.env['NPM_AUTH_TOKEN']}`);
|
||||
|
||||
const isDryRun = process.argv.indexOf('--dry') !== -1;
|
||||
const isDryRun = process.argv.includes('--dry');
|
||||
if (isDryRun) {
|
||||
console.log('Publish dry run');
|
||||
}
|
||||
@@ -22,6 +22,7 @@ const changedFiles = getChangedFilesInCommit('HEAD');
|
||||
let isStableRelease = false;
|
||||
if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
|
||||
isStableRelease = checkAndPublishPackage(path.resolve(__dirname, '..'));
|
||||
checkAndPublishPackage(path.resolve(__dirname, '../headless'));
|
||||
}
|
||||
|
||||
// Publish addons if any files were changed inside of the addon
|
||||
@@ -36,13 +37,13 @@ const addonPackageDirs = [
|
||||
path.resolve(__dirname, '../addons/xterm-addon-webgl')
|
||||
];
|
||||
console.log(`Checking if addons need to be published`);
|
||||
addonPackageDirs.forEach(p => {
|
||||
for (const p of addonPackageDirs) {
|
||||
const addon = path.basename(p);
|
||||
if (changedFiles.some(e => e.indexOf(addon) !== -1)) {
|
||||
if (changedFiles.some(e => e.includes(addon))) {
|
||||
console.log(`Try publish ${addon}`);
|
||||
checkAndPublishPackage(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Publish website if it's a stable release
|
||||
if (isStableRelease) {
|
||||
@@ -54,7 +55,7 @@ function checkAndPublishPackage(packageDir) {
|
||||
|
||||
// Determine if this is a stable or beta release
|
||||
const publishedVersions = getPublishedVersions(packageJson);
|
||||
const isStableRelease = publishedVersions.indexOf(packageJson.version) === -1;
|
||||
const isStableRelease = !publishedVersions.includes(packageJson.version);
|
||||
|
||||
// Get the next version
|
||||
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson);
|
||||
@@ -91,7 +92,7 @@ function checkAndPublishPackage(packageDir) {
|
||||
}
|
||||
|
||||
function getNextBetaVersion(packageJson) {
|
||||
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.exec(packageJson.version)) {
|
||||
if (!/^\d+\.\d+\.\d+$/.exec(packageJson.version)) {
|
||||
console.error('The package.json version must be of the form x.y.z');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -103,11 +104,11 @@ function getNextBetaVersion(packageJson) {
|
||||
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]+$/)));
|
||||
const aVersion = parseInt(a.slice(a.search(/\d+$/)));
|
||||
const bVersion = parseInt(b.slice(b.search(/\d+$/)));
|
||||
return aVersion > bVersion ? -1 : 1;
|
||||
})[0];
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.slice(latestPublishedVersion.search(/\d+$/)), 10);
|
||||
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
|
||||
}
|
||||
|
||||
|
||||
+33
-15
@@ -12,7 +12,7 @@ const env = { ...process.env };
|
||||
env.NODE_PATH = path.resolve(__dirname, '../out');
|
||||
|
||||
let testFiles = [
|
||||
'./addons/**/out/*api.js',
|
||||
'./addons/**/out-test/*api.js',
|
||||
'./out-test/**/*api.js',
|
||||
];
|
||||
|
||||
@@ -21,7 +21,7 @@ let flagArgs = [];
|
||||
|
||||
if (process.argv.length > 2) {
|
||||
const args = process.argv.slice(2);
|
||||
flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat([...val], []));
|
||||
flagArgs = args.filter(e => e.startsWith('--')).map(arg => arg.split('=')).reduce((arr, val) => arr.concat(val.slice(), []));
|
||||
console.info(flagArgs);
|
||||
// ability to inject particular test files via
|
||||
// yarn test [testFileA testFileB ...]
|
||||
@@ -34,19 +34,37 @@ if (process.argv.length > 2) {
|
||||
|
||||
|
||||
env.DEBUG = flagArgs.indexOf('--debug') >= 0 ? 'debug' : '';
|
||||
env.PORT = 3001;
|
||||
|
||||
const run = cp.spawnSync(
|
||||
npmBinScript('mocha'),
|
||||
[...testFiles, ...flagArgs],
|
||||
{
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
env,
|
||||
stdio: 'inherit'
|
||||
const server = cp.spawn('node', ['demo/start'], {
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
env,
|
||||
stdio: 'pipe'
|
||||
})
|
||||
|
||||
server.stdout.on('data', (data) => {
|
||||
// await for the server to fully start
|
||||
if (data.includes("successfully")) {
|
||||
const run = cp.spawnSync(
|
||||
npmBinScript('mocha'),
|
||||
[...testFiles, ...flagArgs], {
|
||||
cwd: path.resolve(__dirname, '..'),
|
||||
env,
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
|
||||
function npmBinScript(script) {
|
||||
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
|
||||
`${script}.cmd` : script));
|
||||
}
|
||||
|
||||
server.kill();
|
||||
|
||||
process.exit(run.status);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
function npmBinScript(script) {
|
||||
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ? `${script}.cmd` : script));
|
||||
}
|
||||
|
||||
process.exit(run.status);
|
||||
server.stderr.on('data', (data) => {
|
||||
console.error(data.toString());
|
||||
});
|
||||
|
||||
@@ -88,19 +88,19 @@ function evalButtonCode(code) {
|
||||
if (code & 64) {
|
||||
button |= 4
|
||||
}
|
||||
let actionS = 'press';
|
||||
let action = 'press';
|
||||
let buttonS = reverseButtons[button];
|
||||
if (button === 3) {
|
||||
buttonS = '<none>';
|
||||
actionS = 'release';
|
||||
action = 'release';
|
||||
}
|
||||
if (move) {
|
||||
actionS = 'move';
|
||||
action = 'move';
|
||||
} else if (4 <= button && button <= 7) {
|
||||
buttonS = 'wheel';
|
||||
actionS = button === 4 ? 'up' : button === 5 ? 'down' : button === 6 ? 'left' : 'right';
|
||||
action = button === 4 ? 'up' : button === 5 ? 'down' : button === 6 ? 'left' : 'right';
|
||||
}
|
||||
return {button: buttonS, action: actionS, modifier};
|
||||
return {button: buttonS, action, modifier};
|
||||
}
|
||||
|
||||
// protocols
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user