Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10e4d075a3 | ||
|
|
35c2d1ac4a | ||
|
|
86ad5198ac | ||
|
|
9a3ee49160 | ||
|
|
3da594abb0 | ||
|
|
72308b4f7f | ||
|
|
c22b3a92d7 | ||
|
|
f685360707 | ||
|
|
0fcd87085b | ||
|
|
8d2e4328ca | ||
|
|
ae6f5754f2 | ||
|
|
086ca58ece | ||
|
|
e635b3d507 | ||
|
|
df548e1106 | ||
|
|
13b8c78947 | ||
|
|
3cc9994a7d | ||
|
|
d95d4b7dd4 | ||
|
|
d261c3b494 | ||
|
|
8c94c97a28 | ||
|
|
46c316b795 | ||
|
|
7535d2e527 | ||
|
|
6cc1708abe | ||
|
|
e623672e81 | ||
|
|
006e0b7513 | ||
|
|
c5b5f3610e | ||
|
|
6571298061 | ||
|
|
9350a8b237 | ||
|
|
f4c263f380 | ||
|
|
9a920a10f3 | ||
|
|
2381096b06 | ||
|
|
0ddb43ac8a | ||
|
|
45705e0614 | ||
|
|
6d6e8845c1 | ||
|
|
5facfe55b8 | ||
|
|
35cd505e9e | ||
|
|
78d3e22052 | ||
|
|
7157b03a7e | ||
|
|
7663e4a4e3 | ||
|
|
ddc3d07bd4 | ||
|
|
f6d3fae25e | ||
|
|
952d0efde2 | ||
|
|
b7c3397471 | ||
|
|
cde4277e84 | ||
|
|
37b54f8e8a | ||
|
|
1304fd8a72 | ||
|
|
7b072893be | ||
|
|
d10d5be222 | ||
|
|
070afc64c8 | ||
|
|
20141518d9 | ||
|
|
d85a57118e | ||
|
|
d7d85870d2 | ||
|
|
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 | ||
|
|
0b09f0bafd | ||
|
|
54bb0ee189 | ||
|
|
68b7f88523 | ||
|
|
a9eb1de4b4 | ||
|
|
4ba7aef441 | ||
|
|
2f07ed9976 | ||
|
|
079702282c | ||
|
|
a2480bae51 | ||
|
|
cd501f9dcc | ||
|
|
354f3e03a2 | ||
|
|
b07ca8bb24 | ||
|
|
22d777c9f9 | ||
|
|
adfeffec76 | ||
|
|
4a1bdad759 | ||
|
|
00ef60ebaf | ||
|
|
0ad9f70645 | ||
|
|
bc1048062d | ||
|
|
b0605e4cd1 | ||
|
|
82a9ee62d7 | ||
|
|
8b2b0f6abc | ||
|
|
0f9240207d | ||
|
|
c015379c51 | ||
|
|
2f88498ad5 | ||
|
|
5e6e65c8da | ||
|
|
157055c03b | ||
|
|
5200766617 | ||
|
|
ee2c5969fa | ||
|
|
eb5f964bcb | ||
|
|
47be9a2df4 | ||
|
|
aee1629c25 | ||
|
|
8587eea54c | ||
|
|
d7ed109f9d | ||
|
|
84dfe8fccc | ||
|
|
6904dc5009 | ||
|
|
b89227d365 | ||
|
|
6c01630090 | ||
|
|
10282e4cb1 | ||
|
|
8518b354a7 | ||
|
|
00a8b80ef7 | ||
|
|
881a4a0dbb | ||
|
|
edee1a1067 |
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
+16
-2
@@ -9,6 +9,7 @@
|
||||
"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",
|
||||
@@ -43,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",
|
||||
@@ -151,6 +153,10 @@
|
||||
"warn",
|
||||
"never"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"prefer-const": "warn",
|
||||
"spaced-comment": [
|
||||
"warn",
|
||||
@@ -160,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
+68
-49
@@ -1,51 +1,70 @@
|
||||
{
|
||||
"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": "test",
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"dependsOn": "npm: watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"group": "vscode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": "npm: 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 *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,17 +166,28 @@ 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.
|
||||
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP and Database services.
|
||||
- [**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.
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
- [**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
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
@@ -183,21 +197,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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import WebSocket = require('ws');
|
||||
import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
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({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
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 = 768;
|
||||
|
||||
describe('FitAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
@@ -45,7 +42,7 @@ describe('FitAddon', () => {
|
||||
|
||||
describe('proposeDimensions', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
@@ -84,7 +81,7 @@ describe('FitAddon', () => {
|
||||
|
||||
describe('fit', () => {
|
||||
afterEach(async () => {
|
||||
return unloadFit();
|
||||
return await unloadFit();
|
||||
});
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
|
||||
@@ -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.4.0",
|
||||
"version": "0.5.1",
|
||||
"description": "Add support for programming ligatures to xterm.js",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
@@ -32,11 +32,11 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"font-finder": "^1.1.0",
|
||||
"font-ligatures": "^1.3.3"
|
||||
"font-ligatures": "^1.4.0"
|
||||
},
|
||||
"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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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--;
|
||||
|
||||
@@ -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,11 +66,10 @@ 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.3"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.3.tgz#6ada78118d8d24caee595595accdc0ac6abd022e"
|
||||
integrity sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==
|
||||
|
||||
font-finder@^1.0.3:
|
||||
version "1.0.4"
|
||||
@@ -87,19 +80,19 @@ font-finder@^1.0.3:
|
||||
|
||||
font-finder@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
|
||||
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.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-ligatures@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/font-ligatures/-/font-ligatures-1.4.0.tgz#6a7b370d96be1358dddfad67830e82fbfd59e6dc"
|
||||
integrity sha512-n7DFnnEpJ0NrVoLqZIL4tMGVs+CnFwQc92m80LWyrbgAFO4x234+t2/H9o4eOYA1eh6ta9dZAEEsJAwsBdNezA==
|
||||
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:
|
||||
@@ -110,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"
|
||||
@@ -128,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"
|
||||
@@ -144,12 +133,12 @@ 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"
|
||||
@@ -163,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"
|
||||
@@ -198,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"
|
||||
@@ -232,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.8.0",
|
||||
"version": "0.8.2",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
|
||||
import { Terminal, IBufferLine, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
@@ -21,9 +21,21 @@ 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 {
|
||||
@@ -34,7 +46,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* 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;
|
||||
@@ -240,8 +252,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])));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,12 +269,12 @@ export class SearchAddon implements ITerminalAddon {
|
||||
*/
|
||||
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;
|
||||
@@ -274,14 +286,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();
|
||||
|
||||
@@ -290,68 +304,108 @@ 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()) {
|
||||
offset += 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
|
||||
@@ -360,23 +414,33 @@ 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];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -390,11 +454,11 @@ export class SearchAddon implements ITerminalAddon {
|
||||
terminal.clearSelection();
|
||||
return false;
|
||||
}
|
||||
terminal.select(result.col, result.row, result.term.length);
|
||||
terminal.select(result.col, result.row, result.size);
|
||||
// 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);
|
||||
scroll -= Math.floor(terminal.rows / 2);
|
||||
terminal.scrollLines(scroll);
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
import { assert } from 'chai';
|
||||
import { readFile } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { openTerminal, 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;
|
||||
@@ -18,10 +18,7 @@ const height = 600;
|
||||
|
||||
describe('Search Tests', function(): void {
|
||||
before(async function(): Promise<any> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
@@ -106,6 +103,23 @@ describe('Search Tests', function(): void {
|
||||
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('Regression tests', () => {
|
||||
describe('#2444 wrapped line content not being found', () => {
|
||||
let fixture: string;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -35,7 +35,7 @@ perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolc
|
||||
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,12 +1,13 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.4.0",
|
||||
"version": "0.6.1",
|
||||
"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",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
|
||||
@@ -7,13 +7,17 @@
|
||||
|
||||
import { Terminal, ITerminalAddon, IBuffer, IBufferCell } from 'xterm';
|
||||
|
||||
|
||||
function constrain(value: number, low: number, high: number): number {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// we need two of them to flip between old and new cell
|
||||
@@ -21,7 +25,7 @@ abstract class BaseSerializeHandler {
|
||||
const cell2 = this._buffer.getNullCell();
|
||||
let oldCell = cell1;
|
||||
|
||||
this._beforeSerialize(endRow - startRow);
|
||||
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||
|
||||
for (let row = startRow; row < endRow; row++) {
|
||||
const line = this._buffer.getLine(row);
|
||||
@@ -36,7 +40,7 @@ abstract class BaseSerializeHandler {
|
||||
oldCell = c;
|
||||
}
|
||||
}
|
||||
this._rowEnd(row);
|
||||
this._rowEnd(row, row === endRow - 1);
|
||||
}
|
||||
|
||||
this._afterSerialize();
|
||||
@@ -45,8 +49,8 @@ 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 ''; }
|
||||
}
|
||||
@@ -68,30 +72,157 @@ function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||
&& 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, oldCell: IBufferCell): number[] {
|
||||
const sgrSeq: number[] = [];
|
||||
const fgChanged = !equalFg(cell, oldCell);
|
||||
const bgChanged = !equalBg(cell, oldCell);
|
||||
@@ -99,7 +230,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 +260,133 @@ 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);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,21 +399,73 @@ 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(maxRows - correctRows, maxRows);
|
||||
}
|
||||
|
||||
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 dispose(): void { }
|
||||
}
|
||||
|
||||
|
||||
interface ISerializeOptions {
|
||||
scrollback?: number;
|
||||
excludeModes?: boolean;
|
||||
excludeAltBuffer?: boolean;
|
||||
}
|
||||
|
||||
@@ -4,22 +4,41 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
import { openTerminal, 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;
|
||||
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> {
|
||||
const browserType = getBrowserType();
|
||||
browser = await browserType.launch({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
@@ -27,19 +46,72 @@ describe('SerializeAddon', () => {
|
||||
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, '1234567890n12345');
|
||||
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> {
|
||||
const rows = 10;
|
||||
const cols = 10;
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), '');
|
||||
});
|
||||
|
||||
it('trim last empty lines', async function(): Promise<any> {
|
||||
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 = [
|
||||
'',
|
||||
@@ -55,7 +127,7 @@ describe('SerializeAddon', () => {
|
||||
''
|
||||
];
|
||||
await writeSync(page, lines.join('\\r\\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.slice(0, 8).join('\r\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('digits content', async function(): Promise<any> {
|
||||
@@ -67,21 +139,34 @@ describe('SerializeAddon', () => {
|
||||
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;
|
||||
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(${halfRows});`), lines.slice(halfRows, 2 * halfRows).join('\r\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize({ scrollback: ${halfScrollback} });`), lines.slice(halfScrollback, rows).join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize 0 rows of content', async function(): Promise<any> {
|
||||
const rows = 10;
|
||||
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(0);`), '');
|
||||
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> {
|
||||
@@ -105,17 +190,19 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -137,16 +224,16 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -156,19 +243,19 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -178,16 +265,16 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -197,19 +284,19 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -219,16 +306,16 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -238,19 +325,19 @@ describe('SerializeAddon', () => {
|
||||
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
|
||||
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'));
|
||||
@@ -276,17 +363,15 @@ describe('SerializeAddon', () => {
|
||||
'中文中文',
|
||||
'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中文中文',
|
||||
'中'
|
||||
// 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();`), expected.join('\r\n'));
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize CJK Mixed with tab correctly', async () => {
|
||||
@@ -299,15 +384,173 @@ describe('SerializeAddon', () => {
|
||||
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] = (<(index: number) => T>initial)(i);
|
||||
array[i] = (initial as (index: number) => T)(i);
|
||||
} else {
|
||||
array[i] = <T>initial;
|
||||
array[i] = initial as T;
|
||||
}
|
||||
}
|
||||
return array;
|
||||
@@ -321,7 +564,7 @@ function digitsString(length: number, from: number = 0, sgr: string = ''): strin
|
||||
return s;
|
||||
}
|
||||
|
||||
function mkSGR(...seq: string[]): string {
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
}
|
||||
|
||||
@@ -350,20 +593,20 @@ 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 DIM = '2';
|
||||
const ITALIC = '3';
|
||||
const UNDERLINED = '4';
|
||||
const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
const STRIKETHROUGH = '9';
|
||||
|
||||
const NO_INVERSE = '27';
|
||||
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 ITALIC = '3';
|
||||
const DIM = '2';
|
||||
|
||||
const NO_ITALIC = '23';
|
||||
const NO_DIM = '22';
|
||||
const NO_STRIKETHROUGH = '29';
|
||||
|
||||
+31
-10
@@ -3,36 +3,57 @@
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/"
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
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({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
|
||||
@@ -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.5.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -3,25 +3,45 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkProvider, IBufferCellPosition, ILink, Terminal } from 'xterm';
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||
|
||||
interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -69,7 +89,7 @@ export class LinkComputer {
|
||||
}
|
||||
};
|
||||
|
||||
result.push({ range, text, activate: handler });
|
||||
result.push({ range, text, activate });
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
|
||||
import { WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
@@ -29,13 +29,23 @@ 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');
|
||||
}
|
||||
}
|
||||
|
||||
interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
urlRegex?: RegExp;
|
||||
}
|
||||
|
||||
export class WebLinksAddon implements ITerminalAddon {
|
||||
private _linkMatcherId: number | undefined;
|
||||
private _terminal: Terminal | undefined;
|
||||
@@ -43,20 +53,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, pollFor, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||
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({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
|
||||
@@ -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,21 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,29 @@
|
||||
**/*.api.js
|
||||
**/*.api.ts
|
||||
tsconfig.json
|
||||
.yarnrc
|
||||
webpack.config.js
|
||||
out-test/
|
||||
test/
|
||||
# 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.11.4",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -176,8 +176,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;
|
||||
}
|
||||
@@ -263,7 +264,7 @@ export class GlyphRenderer {
|
||||
// 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));
|
||||
fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE);
|
||||
switch (workCell.getBgColorMode()) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
* @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';
|
||||
|
||||
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 +24,15 @@ 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 colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ 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 { Content, 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';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
@@ -41,9 +45,13 @@ 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,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -52,7 +60,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 +90,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 +105,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 +119,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 +147,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 +162,13 @@ 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();
|
||||
if (this._model.selection.hasSelection) {
|
||||
// Update selection as dimensions have changed
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
}
|
||||
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
|
||||
@@ -159,7 +176,6 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
this._model.clearSelection();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
@@ -167,30 +183,37 @@ 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 });
|
||||
}
|
||||
|
||||
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 +230,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 +243,13 @@ 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 });
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderLayers.forEach(l => l.reset(this._terminal));
|
||||
for (const l of this._renderLayers) {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
@@ -248,12 +272,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
|
||||
@@ -266,16 +292,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
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;
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
@@ -284,33 +335,52 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// 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] === cell.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.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] = cell.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = cell.fg;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workCell.bg, this._workCell.fg, chars);
|
||||
this._glyphRenderer.updateCell(x, y, code, cell.bg, cell.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._workCell.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
if (this._model.selection.hasSelection) {
|
||||
// Model could be updated but the selection is unchanged
|
||||
this._glyphRenderer.updateSelection(this._model);
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -323,6 +393,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -334,6 +405,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
|
||||
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -408,3 +481,49 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
|
||||
@@ -13,7 +13,7 @@ const NULL_COLOR: IColor = {
|
||||
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,
|
||||
@@ -28,7 +28,12 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
|
||||
contrastCache: colors.contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: terminal.getOption('customGlyphs'),
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
letterSpacing: terminal.getOption('letterSpacing'),
|
||||
lineHeight: terminal.getOption('lineHeight'),
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.getOption('fontFamily'),
|
||||
@@ -49,6 +54,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,7 +4,7 @@
|
||||
*/
|
||||
|
||||
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';
|
||||
@@ -12,9 +12,10 @@ import { IColor } from 'browser/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { channels, rgba } from 'browser/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
|
||||
// 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 +81,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 {
|
||||
@@ -317,6 +318,13 @@ 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.scaledCharWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
@@ -331,6 +339,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,7 +368,7 @@ 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);
|
||||
|
||||
@@ -367,8 +377,66 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no
|
||||
// padding on either side that are allowed for other glyphs since they are designed to be pixel
|
||||
// perfect but may render with "bad" anti-aliasing
|
||||
let isPowerlineGlyph = false;
|
||||
if (chars.length === 1) {
|
||||
const code = chars.charCodeAt(0);
|
||||
if (code >= 0xE0A0 && code <= 0xE0D6) {
|
||||
isPowerlineGlyph = true;
|
||||
}
|
||||
}
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = isPowerlineGlyph ? 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 +459,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, isPowerlineGlyph, 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 +492,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.scaledCharHeight : 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 +512,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 +525,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 +540,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 +567,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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,25 +68,18 @@ 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 {
|
||||
@@ -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;
|
||||
}
|
||||
@@ -193,7 +186,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
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,
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
import { ITheme } from 'xterm';
|
||||
import { getBrowserType, launchBrowser, openTerminal, pollFor, 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;
|
||||
@@ -890,11 +890,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({
|
||||
headless: process.argv.indexOf('--headless') !== -1
|
||||
});
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
await page.goto(APP);
|
||||
|
||||
+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>;
|
||||
}
|
||||
}
|
||||
|
||||
+28
-16
@@ -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,7 +92,7 @@ jobs:
|
||||
displayName: Install required packages
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -100,10 +100,6 @@ jobs:
|
||||
displayName: 'Install Yarn'
|
||||
- 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"
|
||||
@@ -115,14 +111,10 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '10.x'
|
||||
versionSpec: '14.x'
|
||||
displayName: 'Install Node.js'
|
||||
- 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
|
||||
@@ -130,6 +122,21 @@ jobs:
|
||||
- script: yarn test-api-webkit --headless --forbid-only
|
||||
displayName: 'Integration tests (Webkit)'
|
||||
|
||||
- job: Windows_IntegrationTests
|
||||
pool:
|
||||
vmImage: 'windows-2019'
|
||||
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)'
|
||||
|
||||
- job: Release
|
||||
dependsOn:
|
||||
- Linux
|
||||
@@ -137,13 +144,14 @@ jobs:
|
||||
- Windows
|
||||
- Linux_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:
|
||||
@@ -156,5 +164,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'
|
||||
|
||||
@@ -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 = [
|
||||
@@ -362,7 +362,7 @@ function* parseMultiLineGen(filename, s) {
|
||||
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;
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
+5
-4
@@ -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
|
||||
@@ -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.substr(a.search(/\d+$/)));
|
||||
const bVersion = parseInt(b.substr(b.search(/\d+$/)));
|
||||
return aVersion > bVersion ? -1 : 1;
|
||||
})[0];
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/[0-9]+$/)), 10);
|
||||
const latestTagVersion = parseInt(latestPublishedVersion.substr(latestPublishedVersion.search(/\d+$/)), 10);
|
||||
return `${nextStableVersion}-${tag}.${latestTagVersion + 1}`;
|
||||
}
|
||||
|
||||
|
||||
+32
-14
@@ -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());
|
||||
});
|
||||
|
||||
+6
-2
@@ -36,7 +36,6 @@
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
font-feature-settings: "liga" 0;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
@@ -134,7 +133,8 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer {
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -169,3 +169,7 @@
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
+118
-15
@@ -16,6 +16,7 @@ import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAdd
|
||||
import { WebLinksAddon } from '../addons/xterm-addon-web-links/out/WebLinksAddon';
|
||||
import { WebglAddon } from '../addons/xterm-addon-webgl/out/WebglAddon';
|
||||
import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Addon';
|
||||
import { LigaturesAddon } from '../addons/xterm-addon-ligatures/out/LigaturesAddon';
|
||||
|
||||
// Use webpacked version (yarn package)
|
||||
// import { Terminal } from '../lib/xterm';
|
||||
@@ -26,6 +27,7 @@ import { Unicode11Addon } from '../addons/xterm-addon-unicode11/out/Unicode11Add
|
||||
// import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
// import { WebglAddon } from 'xterm-addon-webgl';
|
||||
// import { Unicode11Addon } from 'xterm-addon-unicode11';
|
||||
// import { LigaturesAddon } from 'xterm-addon-ligatures';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
@@ -41,6 +43,7 @@ export interface IWindowWithTerminal extends Window {
|
||||
WebLinksAddon?: typeof WebLinksAddon;
|
||||
WebglAddon?: typeof WebglAddon;
|
||||
Unicode11Addon?: typeof Unicode11Addon;
|
||||
LigaturesAddon?: typeof LigaturesAddon;
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
@@ -50,7 +53,7 @@ let socketURL;
|
||||
let socket;
|
||||
let pid;
|
||||
|
||||
type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl';
|
||||
type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures';
|
||||
|
||||
interface IDemoAddon<T extends AddonType> {
|
||||
name: T;
|
||||
@@ -62,8 +65,9 @@ interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'serialize' ? typeof SerializeAddon :
|
||||
T extends 'web-links' ? typeof WebLinksAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
typeof WebglAddon;
|
||||
instance?:
|
||||
instance?:
|
||||
T extends 'attach' ? AttachAddon :
|
||||
T extends 'fit' ? FitAddon :
|
||||
T extends 'search' ? SearchAddon :
|
||||
@@ -71,6 +75,7 @@ interface IDemoAddon<T extends AddonType> {
|
||||
T extends 'web-links' ? WebLinksAddon :
|
||||
T extends 'webgl' ? WebglAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
never;
|
||||
}
|
||||
|
||||
@@ -81,7 +86,8 @@ const addons: { [T in AddonType]: IDemoAddon<T>} = {
|
||||
serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true },
|
||||
'web-links': { name: 'web-links', ctor: WebLinksAddon, canChange: true },
|
||||
webgl: { name: 'webgl', ctor: WebglAddon, canChange: true },
|
||||
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true }
|
||||
unicode11: { name: 'unicode11', ctor: Unicode11Addon, canChange: true },
|
||||
ligatures: { name: 'ligatures', ctor: LigaturesAddon, canChange: true }
|
||||
};
|
||||
|
||||
const terminalContainer = document.getElementById('terminal-container');
|
||||
@@ -117,6 +123,7 @@ const disposeRecreateButtonHandler = () => {
|
||||
addons.search.instance = undefined;
|
||||
addons.serialize.instance = undefined;
|
||||
addons.unicode11.instance = undefined;
|
||||
addons.ligatures.instance = undefined;
|
||||
addons['web-links'].instance = undefined;
|
||||
addons.webgl.instance = undefined;
|
||||
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
||||
@@ -133,12 +140,15 @@ if (document.location.pathname === '/test') {
|
||||
window.SearchAddon = SearchAddon;
|
||||
window.SerializeAddon = SerializeAddon;
|
||||
window.Unicode11Addon = Unicode11Addon;
|
||||
window.LigaturesAddon = LigaturesAddon;
|
||||
window.WebLinksAddon = WebLinksAddon;
|
||||
window.WebglAddon = WebglAddon;
|
||||
} else {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
document.getElementById('serialize').addEventListener('click', serializeButtonHandler);
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
}
|
||||
|
||||
function createTerminal(): void {
|
||||
@@ -149,7 +159,8 @@ function createTerminal(): void {
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
windowsMode: isWindows
|
||||
windowsMode: isWindows,
|
||||
fontFamily: 'Fira Code, courier-new, courier, monospace'
|
||||
} as ITerminalOptions);
|
||||
|
||||
// Load addons
|
||||
@@ -282,11 +293,11 @@ function initOptions(term: TerminalType): void {
|
||||
rendererType: ['dom', 'canvas'],
|
||||
wordSeparator: null
|
||||
};
|
||||
const options = Object.keys((<any>term)._core.options);
|
||||
const options = Object.getOwnPropertyNames(term.options);
|
||||
const booleanOptions = [];
|
||||
const numberOptions = [];
|
||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||
switch (typeof term.getOption(o)) {
|
||||
switch (typeof term.options[o]) {
|
||||
case 'boolean':
|
||||
booleanOptions.push(o);
|
||||
break;
|
||||
@@ -303,18 +314,18 @@ function initOptions(term: TerminalType): void {
|
||||
let html = '';
|
||||
html += '<div class="option-group">';
|
||||
booleanOptions.forEach(o => {
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.options[o] ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.options[o]}" step="${o === 'lineHeight' || o === 'scrollSensitivity' ? '0.1' : '1'}"/></label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
if (stringOptions[o]) {
|
||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.options[o] === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||
} else {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`;
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.options[o]}"/></label></div>`;
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
@@ -327,7 +338,7 @@ function initOptions(term: TerminalType): void {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.checked);
|
||||
term.setOption(o, input.checked);
|
||||
term.options[o] = input.checked;
|
||||
});
|
||||
});
|
||||
numberOptions.forEach(o => {
|
||||
@@ -336,11 +347,17 @@ function initOptions(term: TerminalType): void {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'cols' || o === 'rows') {
|
||||
updateTerminalSize();
|
||||
} else if (o === 'lineHeight' || o === 'scrollSensitivity') {
|
||||
term.setOption(o, parseFloat(input.value));
|
||||
} else if (o === 'lineHeight') {
|
||||
term.options.lineHeight = parseFloat(input.value);
|
||||
updateTerminalSize();
|
||||
} else if (o === 'scrollSensitivity') {
|
||||
term.options.scrollSensitivity = parseFloat(input.value);
|
||||
updateTerminalSize();
|
||||
} else if(o === 'scrollback') {
|
||||
term.options.scrollback = parseInt(input.value);
|
||||
setTimeout(() => updateTerminalSize(), 5);
|
||||
} else {
|
||||
term.setOption(o, parseInt(input.value));
|
||||
term.options[o] = parseInt(input.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -348,7 +365,7 @@ function initOptions(term: TerminalType): void {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
term.setOption(o, input.value);
|
||||
term.options[o] = input.value;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -363,6 +380,9 @@ function initAddons(term: TerminalType): void {
|
||||
if (!addon.canChange) {
|
||||
checkbox.disabled = true;
|
||||
}
|
||||
if(name === 'unicode11' && checkbox.checked) {
|
||||
term.unicode.activeVersion = '11';
|
||||
}
|
||||
addDomListener(checkbox, 'change', () => {
|
||||
if (checkbox.checked) {
|
||||
addon.instance = new addon.ctor();
|
||||
@@ -371,10 +391,14 @@ function initAddons(term: TerminalType): void {
|
||||
setTimeout(() => {
|
||||
document.body.appendChild((addon.instance as WebglAddon).textureAtlas);
|
||||
}, 0);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
}
|
||||
} else {
|
||||
if (name === 'webgl') {
|
||||
document.body.removeChild((addon.instance as WebglAddon).textureAtlas);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '6';
|
||||
}
|
||||
addon.instance!.dispose();
|
||||
addon.instance = undefined;
|
||||
@@ -422,3 +446,82 @@ function serializeButtonHandler(): void {
|
||||
term.write(output);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function writeCustomGlyphHandler() {
|
||||
term.write('\n\r');
|
||||
term.write('\n\r');
|
||||
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
|
||||
term.write('┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤\n\r');
|
||||
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘\n\r');
|
||||
term.write('├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐\n\r');
|
||||
term.write('│ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤\n\r');
|
||||
term.write('└─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘\n\r');
|
||||
term.write('\n\r');
|
||||
term.write('Other:\n\r');
|
||||
term.write('╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈\n\r');
|
||||
term.write('│ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉\n\r');
|
||||
term.write('╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋\n\r');
|
||||
term.write('\n\r');
|
||||
term.write('All box drawing characters:\n\r');
|
||||
term.write('─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏\n\r');
|
||||
term.write('┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟\n\r');
|
||||
term.write('┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯\n\r');
|
||||
term.write('┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿\n\r');
|
||||
term.write('╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏\n\r');
|
||||
term.write('═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟\n\r');
|
||||
term.write('╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯\n\r');
|
||||
term.write('╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿\n\r');
|
||||
term.write('Box drawing alignment tests:\x1b[31m █\n\r');
|
||||
term.write(' ▉\n\r');
|
||||
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
|
||||
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
|
||||
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
|
||||
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
|
||||
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
|
||||
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
|
||||
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
|
||||
term.write('Box drawing alignment tests:\x1b[32m █\n\r');
|
||||
term.write(' ▉\n\r');
|
||||
term.write(' ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳\n\r');
|
||||
term.write(' ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳\n\r');
|
||||
term.write(' ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳\n\r');
|
||||
term.write(' ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳\n\r');
|
||||
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
|
||||
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
|
||||
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function loadTest() {
|
||||
const isWebglEnabled = !!addons.webgl.instance;
|
||||
const testData = [];
|
||||
let byteCount = 0;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const count = 1 + Math.floor(Math.random() * 79);
|
||||
byteCount += count + 2;
|
||||
const data = new Uint8Array(count + 2);
|
||||
data[0] = 0x0A; // \n
|
||||
for (let i = 1; i < count + 1; i++) {
|
||||
data[i] = 0x61 + Math.floor(Math.random() * (0x7A - 0x61));
|
||||
}
|
||||
// End each line with \r so the cursor remains constant, this is what ls/tree do and improves
|
||||
// performance significantly due to the cursor DOM element not needing to change
|
||||
data[data.length - 1] = 0x0D; // \r
|
||||
testData.push(data);
|
||||
}
|
||||
const start = performance.now();
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
for (const d of testData) {
|
||||
term.write(d);
|
||||
}
|
||||
}
|
||||
// Wait for all data to be parsed before evaluating time
|
||||
term.write('', () => {
|
||||
const time = Math.round(performance.now() - start);
|
||||
const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2);
|
||||
term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`);
|
||||
// Send ^C to get a new prompt
|
||||
term._core._onData.fire('\x03');
|
||||
});
|
||||
}
|
||||
|
||||
+85
-35
@@ -15,41 +15,91 @@
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
|
||||
<div id="terminal-container"></div>
|
||||
<div>
|
||||
<h3>Options</h3>
|
||||
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.setOption</code> function.</p>
|
||||
<div id="options-container"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Addons</h3>
|
||||
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
|
||||
<div id="addons-container"></div>
|
||||
<h3>Addons Control</h3>
|
||||
<h4>SearchAddon</h4>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||
</p>
|
||||
<h4>SerializeAddon</h4>
|
||||
<p>
|
||||
<button id="serialize">Serialize the content of terminal</button>
|
||||
<label><input type="checkbox" id="write-to-terminal">Write back to terminal</label>
|
||||
<div><pre id="serialize-output"></pre></div>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Style</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="number" id="padding" />
|
||||
<div id="container">
|
||||
<div id="grid">
|
||||
<div id="terminal-container"></div>
|
||||
</div>
|
||||
<div id="grid">
|
||||
<div class="tab">
|
||||
<button id= "optionsbutton" class="tabLinks" onclick="openSection(event, 'options')">Options</button>
|
||||
<button id= "addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
|
||||
<button id= "stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
|
||||
<button id= "testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
|
||||
</div>
|
||||
<div id="options" class="tabContent">
|
||||
<h3>Options</h3>
|
||||
<p>These options can be set in the <code>Terminal</code> constructor or by using the <code>Terminal.setOption</code> function.</p>
|
||||
<div id="options-container"></div>
|
||||
</div>
|
||||
<div id="addons" class="tabContent">
|
||||
<h3>Addons</h3>
|
||||
<p>Addons can be loaded and unloaded on a particular terminal to extend its functionality.</p>
|
||||
<div id="addons-container"></div>
|
||||
<h3>Addons Control</h3>
|
||||
<h4>SearchAddon</h4>
|
||||
<div style= "display:flex; flex-direction:column;">
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label><input type="checkbox" id="regex"/>Use regex</label>
|
||||
<label><input type="checkbox" id="case-sensitive"/>Case sensitive</label>
|
||||
<label><input type="checkbox" id="whole-word"/>Whole word</label>
|
||||
</div>
|
||||
<h4>SerializeAddon</h4>
|
||||
<div>
|
||||
<button id="serialize">Serialize the content of terminal</button>
|
||||
<label><input type="checkbox" id="write-to-terminal">Write back to terminal</label>
|
||||
<div><pre id="serialize-output"></pre></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="style" class="tabContent">
|
||||
<h3>Style</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="number" id="padding" />
|
||||
</div>
|
||||
</div>
|
||||
<div id="test" class="tabContent">
|
||||
<h3>Test</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button>
|
||||
<button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
</body>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
<script>
|
||||
var tab = localStorage.getItem("tab");
|
||||
|
||||
if(tab === null){
|
||||
document.getElementById("options").style.display = "block";
|
||||
document.getElementById("optionsbutton").classList.add("active");
|
||||
}
|
||||
else {
|
||||
const tabContent = document.getElementsByClassName("tabContent");
|
||||
let itr;
|
||||
for (itr = 0; itr < tabContent.length; itr+=1) {
|
||||
tabContent[itr].style.display = "none";
|
||||
}
|
||||
document.getElementById(""+tab+"").style.display = "block";
|
||||
document.getElementById(""+tab+"button").classList.add("active");
|
||||
}
|
||||
function openSection(event, section) {
|
||||
const tabContent = document.getElementsByClassName("tabContent");
|
||||
let itr;
|
||||
for (itr = 0; itr < tabContent.length; itr+=1) {
|
||||
tabContent[itr].style.display = "none";
|
||||
}
|
||||
const tabLinks = document.getElementsByClassName("tabLinks");
|
||||
for (itr = 0; itr < tabLinks.length; itr+=1) {
|
||||
tabLinks[itr].className = tabLinks[itr].className.replace(" active", "");
|
||||
}
|
||||
document.getElementById(section).style.display = "block";
|
||||
localStorage.setItem("tab", section);
|
||||
event.currentTarget.className += " active";
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -49,6 +49,14 @@ const clientConfig = {
|
||||
alias: {
|
||||
common: path.resolve('./out/common'),
|
||||
browser: path.resolve('./out/browser')
|
||||
},
|
||||
fallback: {
|
||||
// The ligature modules contains fallbacks for node environments, we never want to browserify them
|
||||
stream: false,
|
||||
util: false,
|
||||
os: false,
|
||||
path: false,
|
||||
fs: false
|
||||
}
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -41,3 +41,52 @@ pre {
|
||||
word-wrap: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
height: 75vh;
|
||||
}
|
||||
#grid {
|
||||
flex: 1;
|
||||
/* max-height: 80vh;
|
||||
overflow-y: auto; */
|
||||
width: 100%;
|
||||
}
|
||||
.tab {
|
||||
overflow: hidden;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
/* Style the buttons inside the tab */
|
||||
.tab button {
|
||||
background-color: inherit;
|
||||
float: left;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
padding: 14px 16px;
|
||||
transition: 0.3s;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
/* Change background color of buttons on hover */
|
||||
.tab button:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
/* Create an active/current tablink class */
|
||||
.tab button.active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
/* Style the tab content */
|
||||
.tabContent {
|
||||
display: none;
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ccc;
|
||||
border-top: none;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
lib-headless/
|
||||
typings/
|
||||
logo-full.png
|
||||
./package.json
|
||||
@@ -0,0 +1,29 @@
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib-headless/
|
||||
!lib-headless/**/*.d.ts
|
||||
|
||||
!lib-headless/**/*.js
|
||||
!lib-headless/**/*.js.map
|
||||
|
||||
!lib-headless/**/*.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
|
||||
@@ -0,0 +1,31 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
⚠ This package is experimental
|
||||
|
||||
`xterm-headless` is a headless terminal that can be run in node.js. This is useful in combination with the frontend [`xterm`](https://www.npmjs.com/package/xterm) for example to keep track of a terminal's state on a remote server where the process is hosted.
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, you need to install the module, we ship exclusively through npm, so you need that installed and then add xterm.js as a dependency by running:
|
||||
|
||||
```sh
|
||||
npm install xterm-headless
|
||||
```
|
||||
|
||||
Then import as you would a regular node package. The recommended way to load `xterm-headless` is with TypeScript and the ES6 module syntax:
|
||||
|
||||
```javascript
|
||||
import { Terminal } from 'xterm-headless';
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
The full API for `xterm-headless` is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm-headless.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.
|
||||
|
||||
### Addons
|
||||
|
||||
Addons in `xterm-headless` work the [same as in `xterm`](https://github.com/xtermjs/xterm.js/blob/master/README.md#addons) with the one caveat being that the addon needs to be packaged for node.js and not use any DOM APIs.
|
||||
|
||||
Currently no official addons are packaged on npm.
|
||||
+35
-29
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "4.10.0",
|
||||
"version": "4.16.0",
|
||||
"main": "lib/xterm.js",
|
||||
"style": "css/xterm.css",
|
||||
"types": "typings/xterm.d.ts",
|
||||
@@ -10,7 +10,10 @@
|
||||
"scripts": {
|
||||
"prepackage": "npm run build",
|
||||
"package": "webpack",
|
||||
"package-headless": "webpack --config ./webpack.config.headless.js",
|
||||
"postpackage-headless": "node ./bin/package_headless.js",
|
||||
"start": "node demo/start",
|
||||
"start-debug": "node --inspect-brk demo/start",
|
||||
"lint": "eslint -c .eslintrc.json --max-warnings 0 --ext .ts src/ addons/",
|
||||
"test": "npm run test-unit",
|
||||
"posttest": "npm run lint",
|
||||
@@ -20,6 +23,7 @@
|
||||
"test-api-webkit": "node ./bin/test_api.js --browser=webkit --timeout=20000",
|
||||
"test-unit": "node ./bin/test.js",
|
||||
"test-unit-coverage": "node ./bin/test.js --coverage",
|
||||
"test-unit-dev": "cross-env NODE_PATH='./out' mocha",
|
||||
"build": "tsc -b ./tsconfig.all.json",
|
||||
"prepare": "npm run setup",
|
||||
"setup": "npm run build",
|
||||
@@ -33,37 +37,39 @@
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/**/*.ts src/*.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.2.14",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/chai": "^4.2.22",
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/deep-equal": "^1.0.1",
|
||||
"@types/glob": "^7.1.3",
|
||||
"@types/jsdom": "^16.2.5",
|
||||
"@types/mocha": "^8.0.3",
|
||||
"@types/node": "^10.17.17",
|
||||
"@types/utf8": "^2.1.6",
|
||||
"@types/webpack": "^4.41.24",
|
||||
"@types/ws": "^7.2.9",
|
||||
"@typescript-eslint/eslint-plugin": "^4.0.0",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"chai": "^4.2.0",
|
||||
"deep-equal": "^2.0.4",
|
||||
"eslint": "^7.12.1",
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.44",
|
||||
"@types/utf8": "^3.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.3.0",
|
||||
"@typescript-eslint/parser": "^5.3.0",
|
||||
"chai": "^4.3.4",
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"eslint": "^8.1.0",
|
||||
"express": "^4.17.1",
|
||||
"express-ws": "^4.0.0",
|
||||
"glob": "^7.0.5",
|
||||
"jsdom": "^16.4.0",
|
||||
"mocha": "^8.2.1",
|
||||
"mustache": "^4.0.1",
|
||||
"node-pty": "^0.9.0",
|
||||
"express-ws": "^5.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"jsdom": "^18.0.1",
|
||||
"mocha": "^9.1.3",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
"playwright": "^1.5.2",
|
||||
"source-map-loader": "^1.1.2",
|
||||
"ts-loader": "^8.0.8",
|
||||
"typescript": "4.0",
|
||||
"playwright": "^1.16.2",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.1.2",
|
||||
"typescript": "^4.4.4",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.4.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"ws": "^7.3.1",
|
||||
"xterm-benchmark": "^0.1.3"
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"ws": "^8.2.3",
|
||||
"xterm-benchmark": "^0.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { ITerminal, IRenderDebouncer } from 'browser/Types';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { RenderDebouncer } from 'browser/RenderDebouncer';
|
||||
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
@@ -28,7 +28,7 @@ export class AccessibilityManager extends Disposable {
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
private _renderRowsDebouncer: IRenderDebouncer;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
@@ -53,7 +53,9 @@ export class AccessibilityManager extends Disposable {
|
||||
) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.setAttribute('role', 'document');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
this._accessibilityTreeRoot.tabIndex = 0;
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.setAttribute('role', 'list');
|
||||
@@ -72,7 +74,7 @@ export class AccessibilityManager extends Disposable {
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._renderRows.bind(this));
|
||||
this._renderRowsDebouncer = new TimeBasedDebouncer(this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
@@ -112,7 +114,7 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = <HTMLElement>e.target;
|
||||
const boundaryElement = e.target as HTMLElement;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { IColor } from 'browser/Types';
|
||||
import { IColorRGB } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Helper functions where the source type is "channels" (individual color channels as numbers).
|
||||
@@ -17,6 +18,8 @@ export namespace channels {
|
||||
}
|
||||
|
||||
export function toRgba(r: number, g: number, b: number, a: number = 0xFF): number {
|
||||
// Note: The aggregated number is RGBA32 (BE), thus needs to be converted to ABGR32
|
||||
// on LE systems, before it can be used for direct 32-bit buffer writes.
|
||||
// >>> 0 forces an unsigned int
|
||||
return (r << 24 | g << 16 | b << 8 | a) >>> 0;
|
||||
}
|
||||
@@ -81,6 +84,10 @@ export namespace color {
|
||||
rgba: channels.toRgba(r, g, b, a)
|
||||
};
|
||||
}
|
||||
|
||||
export function toColorRGB(color: IColor): IColorRGB {
|
||||
return [(color.rgba >> 24) & 0xFF, (color.rgba >> 16) & 0xFF, (color.rgba >> 8) & 0xFF];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,6 +204,7 @@ export namespace rgba {
|
||||
return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0;
|
||||
}
|
||||
|
||||
// FIXME: Move this to channels NS?
|
||||
export function toChannels(value: number): [number, number, number, number] {
|
||||
return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF];
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('ColorManager', () => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
(<any>window).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
@@ -36,7 +36,7 @@ describe('ColorManager', () => {
|
||||
for (const key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi' && key !== 'contrastCache') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok((<any>cm.colors)[key].css.length >= 7);
|
||||
assert.ok((cm.colors as any)[key].css.length >= 7);
|
||||
}
|
||||
}
|
||||
assert.equal(cm.colors.ansi.length, 256);
|
||||
|
||||
@@ -7,6 +7,16 @@ import { IColorManager, IColor, IColorSet, IColorContrastCache } from 'browser/T
|
||||
import { ITheme } from 'common/services/Services';
|
||||
import { channels, color, css } from 'browser/Color';
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
import { ColorIndex } from 'common/Types';
|
||||
|
||||
|
||||
interface IRestoreColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
|
||||
const DEFAULT_BACKGROUND = css.toColor('#000000');
|
||||
@@ -73,6 +83,7 @@ export class ColorManager implements IColorManager {
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _litmusColor: CanvasGradient;
|
||||
private _contrastCache: IColorContrastCache;
|
||||
private _restoreColors!: IRestoreColorSet;
|
||||
|
||||
constructor(document: Document, public allowTransparency: boolean) {
|
||||
const canvas = document.createElement('canvas');
|
||||
@@ -96,6 +107,7 @@ export class ColorManager implements IColorManager {
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
this._updateRestoreColors();
|
||||
}
|
||||
|
||||
public onOptionsChange(key: string): void {
|
||||
@@ -142,6 +154,39 @@ export class ColorManager implements IColorManager {
|
||||
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
|
||||
// Clear our the cache
|
||||
this._contrastCache.clear();
|
||||
this._updateRestoreColors();
|
||||
}
|
||||
|
||||
public restoreColor(slot?: ColorIndex): void {
|
||||
// unset slot restores all ansi colors
|
||||
if (slot === undefined) {
|
||||
for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
|
||||
this.colors.ansi[i] = this._restoreColors.ansi[i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (slot) {
|
||||
case ColorIndex.FOREGROUND:
|
||||
this.colors.foreground = this._restoreColors.foreground;
|
||||
break;
|
||||
case ColorIndex.BACKGROUND:
|
||||
this.colors.background = this._restoreColors.background;
|
||||
break;
|
||||
case ColorIndex.CURSOR:
|
||||
this.colors.cursor = this._restoreColors.cursor;
|
||||
break;
|
||||
default:
|
||||
this.colors.ansi[slot] = this._restoreColors.ansi[slot];
|
||||
}
|
||||
}
|
||||
|
||||
private _updateRestoreColors(): void {
|
||||
this._restoreColors = {
|
||||
foreground: this.colors.foreground,
|
||||
background: this.colors.background,
|
||||
cursor: this.colors.cursor,
|
||||
ansi: [...this.colors.ansi]
|
||||
};
|
||||
}
|
||||
|
||||
private _parseColor(
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('Linkifier', () => {
|
||||
assert.equal(mouseZoneManager.zones[0].y1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y2, 1);
|
||||
// Fires done()
|
||||
mouseZoneManager.zones[0].clickCallback(<any>{});
|
||||
mouseZoneManager.zones[0].clickCallback({} as any);
|
||||
}
|
||||
});
|
||||
linkifier.linkifyRows();
|
||||
@@ -210,7 +210,7 @@ describe('Linkifier', () => {
|
||||
let count = 0;
|
||||
linkifier.registerLinkMatcher(/test/, () => assert.fail(), {
|
||||
validationCallback: (url, cb) => {
|
||||
count += 1;
|
||||
count++;
|
||||
if (count === 2) {
|
||||
done();
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ export class Linkifier implements ILinkifier {
|
||||
if (this._rowsTimeoutId) {
|
||||
clearTimeout(this._rowsTimeoutId);
|
||||
}
|
||||
this._rowsTimeoutId = <number><any>setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency);
|
||||
|
||||
// Cannot use window.setTimeout since tests need to run in node
|
||||
this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency) as any as number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations } from 'browser/Types';
|
||||
import { ILinkifier2, ILinkProvider, IBufferCellPosition, ILink, ILinkifierEvent, ILinkDecorations, ILinkWithState } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
@@ -11,21 +11,12 @@ import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
interface ILinkState {
|
||||
decorations: ILinkDecorations;
|
||||
isHovered: boolean;
|
||||
}
|
||||
|
||||
interface ILinkWithState {
|
||||
link: ILink;
|
||||
state?: ILinkState;
|
||||
}
|
||||
|
||||
export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _element: HTMLElement | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _linkProviders: ILinkProvider[] = [];
|
||||
public get currentLink(): ILinkWithState | undefined { return this._currentLink; }
|
||||
protected _currentLink: ILinkWithState | undefined;
|
||||
private _lastMouseEvent: MouseEvent | undefined;
|
||||
private _linkCacheDisposables: IDisposable[] = [];
|
||||
|
||||
@@ -156,9 +156,7 @@ export class MouseZoneManager extends Disposable implements IMouseZoneManager {
|
||||
private _onTooltip(e: MouseEvent): void {
|
||||
this._tooltipTimeout = undefined;
|
||||
const zone = this._findZoneEventAt(e);
|
||||
if (zone && zone.tooltipCallback) {
|
||||
zone.tooltipCallback(e);
|
||||
}
|
||||
zone?.tooltipCallback(e);
|
||||
}
|
||||
|
||||
private _onMouseDown(e: MouseEvent): void {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IRenderDebouncer } from 'browser/Types';
|
||||
|
||||
/**
|
||||
* Debounces calls to render terminal rows using animation frames.
|
||||
*/
|
||||
export class RenderDebouncer implements IDisposable {
|
||||
export class RenderDebouncer implements IRenderDebouncer {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
|
||||
+308
-389
File diff suppressed because it is too large
Load Diff
+236
-105
@@ -21,8 +21,8 @@
|
||||
* http://linux.die.net/man/7/urxvt
|
||||
*/
|
||||
|
||||
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2 } from 'browser/Types';
|
||||
import { IRenderer, CharacterJoinerHandler } from 'browser/renderer/Types';
|
||||
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types';
|
||||
import { IRenderer } from 'browser/renderer/Types';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { Viewport } from 'browser/Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
|
||||
@@ -39,21 +39,22 @@ import { MouseZoneManager } from 'browser/MouseZoneManager';
|
||||
import { AccessibilityManager } from './AccessibilityManager';
|
||||
import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { DomRenderer } from 'browser/renderer/dom/DomRenderer';
|
||||
import { IKeyboardEvent, KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, IAnsiColorChangeEvent } from 'common/Types';
|
||||
import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types';
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
import { Linkifier2 } from 'browser/Linkifier2';
|
||||
import { CoreBrowserService } from 'browser/services/CoreBrowserService';
|
||||
import { CoreTerminal } from 'common/CoreTerminal';
|
||||
import { ITerminalOptions as IInitializedTerminalOptions } from 'common/services/Services';
|
||||
import { rgba } from 'browser/Color';
|
||||
import { color, rgba } from 'browser/Color';
|
||||
import { CharacterJoinerService } from 'browser/services/CharacterJoinerService';
|
||||
import { toRgbString } from 'common/input/XParseColor';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
|
||||
@@ -71,10 +72,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
// private _visualBellTimer: number;
|
||||
|
||||
public browser: IBrowser = <any>Browser;
|
||||
|
||||
// TODO: We should remove options once components adopt optionsService
|
||||
public get options(): IInitializedTerminalOptions { return this.optionsService.options; }
|
||||
public browser: IBrowser = Browser as any;
|
||||
|
||||
private _customKeyEventHandler: CustomKeyEventHandler | undefined;
|
||||
|
||||
@@ -82,6 +80,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _charSizeService: ICharSizeService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _characterJoinerService: ICharacterJoinerService | undefined;
|
||||
private _selectionService: ISelectionService | undefined;
|
||||
private _soundService: ISoundService | undefined;
|
||||
|
||||
@@ -92,6 +91,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
*/
|
||||
private _keyDownHandled: boolean = false;
|
||||
|
||||
/**
|
||||
* Records whether the keypress event has already been handled and triggered a data event, if so
|
||||
* the input event should not trigger a data event but should still print to the textarea so
|
||||
* screen readers will announce it.
|
||||
*/
|
||||
private _keyPressHandled: boolean = false;
|
||||
|
||||
/**
|
||||
* Records whether there has been a keydown event for a dead key without a corresponding keydown
|
||||
* event for the composed/alternative character. If we cancel the keydown event for the dead key,
|
||||
* no events will be emitted for the final character.
|
||||
*/
|
||||
private _unprocessedDeadKey: boolean = false;
|
||||
|
||||
public linkifier: ILinkifier;
|
||||
public linkifier2: ILinkifier2;
|
||||
public viewport: IViewport | undefined;
|
||||
@@ -111,6 +124,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
private _onBell = new EventEmitter<void>();
|
||||
public get onBell(): IEvent<void> { return this._onBell.event; }
|
||||
|
||||
private _onFocus = new EventEmitter<void>();
|
||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
||||
@@ -134,7 +149,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* @alias module:xterm/src/xterm
|
||||
*/
|
||||
constructor(
|
||||
options: ITerminalOptions = {}
|
||||
options: Partial<ITerminalOptions> = {}
|
||||
) {
|
||||
super(options);
|
||||
|
||||
@@ -146,10 +161,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Setup InputHandler listeners
|
||||
this.register(this._inputHandler.onRequestBell(() => this.bell()));
|
||||
this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end)));
|
||||
this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus()));
|
||||
this.register(this._inputHandler.onRequestReset(() => this.reset()));
|
||||
this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined)));
|
||||
this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type)));
|
||||
this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event)));
|
||||
this.register(this._inputHandler.onColor((event) => this._handleColorEvent(event)));
|
||||
this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove));
|
||||
this.register(forwardEvent(this._inputHandler.onTitleChange, this._onTitleChange));
|
||||
this.register(forwardEvent(this._inputHandler.onA11yChar, this._onA11yCharEmitter));
|
||||
@@ -159,17 +174,55 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
|
||||
}
|
||||
|
||||
private _changeAnsiColor(event: IAnsiColorChangeEvent): void {
|
||||
if (!this._colorManager) { return; }
|
||||
|
||||
event.colors.forEach(ansiColor => {
|
||||
const color = rgba.toColor(ansiColor.red, ansiColor.green, ansiColor.blue);
|
||||
|
||||
this._colorManager!.colors.ansi[ansiColor.colorIndex] = color;
|
||||
});
|
||||
|
||||
this._renderService?.setColors(this._colorManager!.colors);
|
||||
this.viewport?.onThemeChange(this._colorManager!.colors);
|
||||
/**
|
||||
* Handle color event from inputhandler for OSC 4|104 | 10|110 | 11|111 | 12|112.
|
||||
* An event from OSC 4|104 may contain multiple set or report requests, and multiple
|
||||
* or none restore requests (resetting all),
|
||||
* while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request.
|
||||
*/
|
||||
private _handleColorEvent(event: IColorEvent): void {
|
||||
if (!this._colorManager) return;
|
||||
for (const req of event) {
|
||||
let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
|
||||
let ident = '';
|
||||
switch (req.index) {
|
||||
case ColorIndex.FOREGROUND: // OSC 10 | 110
|
||||
acc = 'foreground';
|
||||
ident = '10';
|
||||
break;
|
||||
case ColorIndex.BACKGROUND: // OSC 11 | 111
|
||||
acc = 'background';
|
||||
ident = '11';
|
||||
break;
|
||||
case ColorIndex.CURSOR: // OSC 12 | 112
|
||||
acc = 'cursor';
|
||||
ident = '12';
|
||||
break;
|
||||
default: // OSC 4 | 104
|
||||
// we can skip the [0..255] range check here (already done in inputhandler)
|
||||
acc = 'ansi';
|
||||
ident = '4;' + req.index;
|
||||
}
|
||||
if (acc) {
|
||||
switch (req.type) {
|
||||
case ColorRequestType.REPORT:
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
? this._colorManager.colors.ansi[req.index]
|
||||
: this._colorManager.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C0.BEL}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
|
||||
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
|
||||
break;
|
||||
case ColorRequestType.RESTORE:
|
||||
this._colorManager.restoreColor(req.index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._renderService?.setColors(this._colorManager.colors);
|
||||
this.viewport?.onThemeChange(this._colorManager.colors);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
@@ -221,6 +274,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// The DOM renderer needs a row refresh to update the cursor styles
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
break;
|
||||
case 'customGlyphs':
|
||||
case 'drawBoldTextInBrightColors':
|
||||
case 'letterSpacing':
|
||||
case 'lineHeight':
|
||||
@@ -264,8 +318,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* Binds the desired focus behavior on a given terminal object.
|
||||
*/
|
||||
private _onTextAreaFocus(ev: KeyboardEvent): void {
|
||||
if (this._coreService.decPrivateModes.sendFocus) {
|
||||
this._coreService.triggerDataEvent(C0.ESC + '[I');
|
||||
if (this.coreService.decPrivateModes.sendFocus) {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[I');
|
||||
}
|
||||
this.updateCursorStyle(ev);
|
||||
this.element!.classList.add('focus');
|
||||
@@ -289,27 +343,34 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// screen readers reading it out.
|
||||
this.textarea!.value = '';
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
if (this._coreService.decPrivateModes.sendFocus) {
|
||||
this._coreService.triggerDataEvent(C0.ESC + '[O');
|
||||
if (this.coreService.decPrivateModes.sendFocus) {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[O');
|
||||
}
|
||||
this.element!.classList.remove('focus');
|
||||
this._onBlur.fire();
|
||||
}
|
||||
|
||||
private _syncTextArea(): void {
|
||||
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing) {
|
||||
if (!this.textarea || !this.buffer.isCursorInViewport || this._compositionHelper!.isComposing || !this._renderService) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cellHeight = Math.ceil(this._charSizeService!.height * this.optionsService.options.lineHeight);
|
||||
const cursorTop = this._bufferService.buffer.y * cellHeight;
|
||||
const cursorLeft = this._bufferService.buffer.x * this._charSizeService!.width;
|
||||
const cursorY = this.buffer.ybase + this.buffer.y;
|
||||
const bufferLine = this.buffer.lines.get(cursorY);
|
||||
if (!bufferLine) {
|
||||
return;
|
||||
}
|
||||
const cursorX = Math.min(this.buffer.x, this.cols - 1);
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const width = bufferLine.getWidth(cursorX);
|
||||
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
|
||||
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
this.textarea.style.left = cursorLeft + 'px';
|
||||
this.textarea.style.top = cursorTop + 'px';
|
||||
this.textarea.style.width = this._charSizeService!.width + 'px';
|
||||
this.textarea.style.width = cellWidth + 'px';
|
||||
this.textarea.style.height = cellHeight + 'px';
|
||||
this.textarea.style.lineHeight = cellHeight + 'px';
|
||||
this.textarea.style.zIndex = '-5';
|
||||
@@ -330,7 +391,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
copyHandler(event, this._selectionService!);
|
||||
}));
|
||||
const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this._coreService);
|
||||
const pasteHandlerWrapper = (event: ClipboardEvent): void => handlePasteEvent(event, this.textarea!, this.coreService);
|
||||
this.register(addDisposableDomListener(this.textarea!, 'paste', pasteHandlerWrapper));
|
||||
this.register(addDisposableDomListener(this.element!, 'paste', pasteHandlerWrapper));
|
||||
|
||||
@@ -372,6 +433,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(addDisposableDomListener(this.textarea!, 'compositionstart', () => this._compositionHelper!.compositionstart()));
|
||||
this.register(addDisposableDomListener(this.textarea!, 'compositionupdate', (e: CompositionEvent) => this._compositionHelper!.compositionupdate(e)));
|
||||
this.register(addDisposableDomListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend()));
|
||||
this.register(addDisposableDomListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true));
|
||||
this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements()));
|
||||
this.register(this.onRender(e => this._queueLinkification(e.start, e.end)));
|
||||
}
|
||||
@@ -386,7 +448,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
throw new Error('Terminal requires a parent element.');
|
||||
}
|
||||
|
||||
if (!document.body.contains(parent)) {
|
||||
if (!parent.isConnected) {
|
||||
this._logService.debug('Terminal.open was called on an element that was not attached to the DOM');
|
||||
}
|
||||
|
||||
@@ -398,7 +460,6 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.element.classList.add('terminal');
|
||||
this.element.classList.add('xterm');
|
||||
this.element.setAttribute('tabindex', '0');
|
||||
this.element.setAttribute('role', 'document');
|
||||
parent.appendChild(this.element);
|
||||
|
||||
// Performance: Use a document fragment to build the terminal
|
||||
@@ -438,6 +499,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
|
||||
this._instantiationService.setService(ICharSizeService, this._charSizeService);
|
||||
|
||||
this._theme = this.options.theme || this._theme;
|
||||
this._colorManager = new ColorManager(document, this.options.allowTransparency);
|
||||
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
|
||||
this._colorManager.setTheme(this._theme);
|
||||
|
||||
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
|
||||
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
|
||||
|
||||
const renderer = this._createRenderer();
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
|
||||
this._instantiationService.setService(IRenderService, this._renderService);
|
||||
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
|
||||
this._compositionView = document.createElement('div');
|
||||
this._compositionView.classList.add('composition-view');
|
||||
this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView);
|
||||
@@ -446,26 +521,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
this._theme = this.options.theme || this._theme;
|
||||
this._colorManager = new ColorManager(document, this.options.allowTransparency);
|
||||
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e)));
|
||||
this._colorManager.setTheme(this._theme);
|
||||
|
||||
const renderer = this._createRenderer();
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
|
||||
this._instantiationService.setService(IRenderService, this._renderService);
|
||||
this.register(this._renderService.onRenderedBufferChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
|
||||
this._soundService = this._instantiationService.createInstance(SoundService);
|
||||
this._instantiationService.setService(ISoundService, this._soundService);
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
|
||||
this.viewport = this._instantiationService.createInstance(Viewport,
|
||||
(amount: number, suppressEvent: boolean) => this.scrollLines(amount, suppressEvent),
|
||||
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
|
||||
this._viewportElement,
|
||||
this._viewportScrollArea
|
||||
this._viewportScrollArea,
|
||||
this.element
|
||||
);
|
||||
this.viewport.onThemeChange(this._colorManager.colors);
|
||||
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
|
||||
@@ -482,7 +547,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
|
||||
this.element,
|
||||
this.screenElement));
|
||||
this.screenElement,
|
||||
this.linkifier2
|
||||
));
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
@@ -495,7 +562,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea!.focus();
|
||||
this.textarea!.select();
|
||||
}));
|
||||
this.register(this.onScroll(() => {
|
||||
this.register(this._onScroll.event(ev => {
|
||||
this.viewport!.syncScrollArea();
|
||||
this._selectionService!.refresh();
|
||||
}));
|
||||
@@ -505,13 +572,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.register(this._mouseZoneManager);
|
||||
this.register(this.onScroll(() => this._mouseZoneManager!.clearAll()));
|
||||
this.linkifier.attachToDom(this.element, this._mouseZoneManager);
|
||||
this.linkifier2.attachToDom(this.element, this._mouseService, this._renderService);
|
||||
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
|
||||
|
||||
// This event listener must be registered aftre MouseZoneManager is created
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
|
||||
|
||||
// apply mouse event classes set by escape codes before terminal was attached
|
||||
if (this._coreMouseService.areMouseEventsActive) {
|
||||
if (this.coreMouseService.areMouseEventsActive) {
|
||||
this._selectionService.disable();
|
||||
this.element.classList.add('enable-mouse-events');
|
||||
} else {
|
||||
@@ -586,7 +653,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
let but: CoreMouseButton;
|
||||
let action: CoreMouseAction | undefined;
|
||||
switch ((<any>ev).overrideType || ev.type) {
|
||||
switch ((ev as any).overrideType || ev.type) {
|
||||
case 'mousemove':
|
||||
action = CoreMouseAction.MOVE;
|
||||
if (ev.buttons === undefined) {
|
||||
@@ -629,7 +696,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self._coreMouseService.triggerMouseEvent({
|
||||
return self.coreMouseService.triggerMouseEvent({
|
||||
col: pos.x - 33, // FIXME: why -33 here?
|
||||
row: pos.y - 33,
|
||||
button: but,
|
||||
@@ -668,8 +735,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
},
|
||||
wheel: (ev: WheelEvent) => {
|
||||
sendEvent(ev);
|
||||
ev.preventDefault();
|
||||
return this.cancel(ev);
|
||||
return this.cancel(ev, true);
|
||||
},
|
||||
mousedrag: (ev: MouseEvent) => {
|
||||
// deal only with move while a button is held
|
||||
@@ -684,11 +750,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}
|
||||
};
|
||||
this.register(this._coreMouseService.onProtocolChange(events => {
|
||||
this.register(this.coreMouseService.onProtocolChange(events => {
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this.optionsService.options.logLevel === 'debug') {
|
||||
this._logService.debug('Binding to mouse events:', this._coreMouseService.explainEvents(events));
|
||||
this._logService.debug('Binding to mouse events:', this.coreMouseService.explainEvents(events));
|
||||
}
|
||||
this.element!.classList.add('enable-mouse-events');
|
||||
this._selectionService!.disable();
|
||||
@@ -731,7 +797,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}));
|
||||
// force initial onProtocolChange so we dont miss early mouse requests
|
||||
this._coreMouseService.activeProtocol = this._coreMouseService.activeProtocol;
|
||||
this.coreMouseService.activeProtocol = this.coreMouseService.activeProtocol;
|
||||
|
||||
/**
|
||||
* "Always on" event listeners.
|
||||
@@ -743,7 +809,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Don't send the mouse button to the pty if mouse events are disabled or
|
||||
// if the selection manager is having selection forced (ie. a modifier is
|
||||
// held).
|
||||
if (!this._coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
|
||||
if (!this.coreMouseService.areMouseEventsActive || this._selectionService!.shouldForceSelection(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -764,46 +830,44 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}));
|
||||
|
||||
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
if (!requestedEvents.wheel) {
|
||||
// do nothing, if app side handles wheel itself
|
||||
if (requestedEvents.wheel) return;
|
||||
|
||||
if (!this.buffer.hasScrollback) {
|
||||
// Convert wheel events into up/down events when the buffer does not have scrollback, this
|
||||
// enables scrolling in apps hosted in the alt buffer such as vim or tmux.
|
||||
if (!this.buffer.hasScrollback) {
|
||||
const amount = this.viewport!.getLinesScrolled(ev);
|
||||
const amount = this.viewport!.getLinesScrolled(ev);
|
||||
|
||||
// Do nothing if there's no vertical scroll
|
||||
if (amount === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this._coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
let data = '';
|
||||
for (let i = 0; i < Math.abs(amount); i++) {
|
||||
data += sequence;
|
||||
}
|
||||
this._coreService.triggerDataEvent(data, true);
|
||||
// Do nothing if there's no vertical scroll
|
||||
if (amount === 0) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}, { passive: true }));
|
||||
|
||||
// allow wheel scrolling in
|
||||
// the shell for example
|
||||
this.register(addDisposableDomListener(el, 'wheel', (ev: WheelEvent) => {
|
||||
if (requestedEvents.wheel) return;
|
||||
if (!this.viewport!.onWheel(ev)) {
|
||||
// Construct and send sequences
|
||||
const sequence = C0.ESC + (this.coreService.decPrivateModes.applicationCursorKeys ? 'O' : '[') + (ev.deltaY < 0 ? 'A' : 'B');
|
||||
let data = '';
|
||||
for (let i = 0; i < Math.abs(amount); i++) {
|
||||
data += sequence;
|
||||
}
|
||||
this.coreService.triggerDataEvent(data, true);
|
||||
return this.cancel(ev, true);
|
||||
}
|
||||
|
||||
// normal viewport scrolling
|
||||
// conditionally stop event, if the viewport still had rows to scroll within
|
||||
if (this.viewport!.onWheel(ev)) {
|
||||
return this.cancel(ev);
|
||||
}
|
||||
}, { passive: false }));
|
||||
|
||||
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
|
||||
if (this._coreMouseService.areMouseEventsActive) return;
|
||||
if (this.coreMouseService.areMouseEventsActive) return;
|
||||
this.viewport!.onTouchStart(ev);
|
||||
return this.cancel(ev);
|
||||
}, { passive: true }));
|
||||
|
||||
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
|
||||
if (this._coreMouseService.areMouseEventsActive) return;
|
||||
if (this.coreMouseService.areMouseEventsActive) return;
|
||||
if (!this.viewport!.onTouchMove(ev)) {
|
||||
return this.cancel(ev);
|
||||
}
|
||||
@@ -834,7 +898,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* Change the cursor style for different selection modes
|
||||
*/
|
||||
public updateCursorStyle(ev: KeyboardEvent): void {
|
||||
if (this._selectionService && this._selectionService.shouldColumnSelect(ev)) {
|
||||
if (this._selectionService?.shouldColumnSelect(ev)) {
|
||||
this.element!.classList.add('column-select');
|
||||
} else {
|
||||
this.element!.classList.remove('column-select');
|
||||
@@ -845,19 +909,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* Display the cursor element
|
||||
*/
|
||||
private _showCursor(): void {
|
||||
if (!this._coreService.isCursorInitialized) {
|
||||
this._coreService.isCursorInitialized = true;
|
||||
if (!this.coreService.isCursorInitialized) {
|
||||
this.coreService.isCursorInitialized = true;
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
}
|
||||
}
|
||||
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean): void {
|
||||
super.scrollLines(disp, suppressScrollEvent);
|
||||
public scrollLines(disp: number, suppressScrollEvent?: boolean, source = ScrollSource.TERMINAL): void {
|
||||
super.scrollLines(disp, suppressScrollEvent, source);
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
|
||||
public paste(data: string): void {
|
||||
paste(data, this.textarea!, this._coreService);
|
||||
paste(data, this.textarea!, this.coreService);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -904,13 +968,19 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
const joinerId = this._renderService!.registerCharacterJoiner(handler);
|
||||
if (!this._characterJoinerService) {
|
||||
throw new Error('Terminal must be opened first');
|
||||
}
|
||||
const joinerId = this._characterJoinerService.register(handler);
|
||||
this.refresh(0, this.rows - 1);
|
||||
return joinerId;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): void {
|
||||
if (this._renderService!.deregisterCharacterJoiner(joinerId)) {
|
||||
if (!this._characterJoinerService) {
|
||||
throw new Error('Terminal must be opened first');
|
||||
}
|
||||
if (this._characterJoinerService.deregister(joinerId)) {
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
}
|
||||
@@ -999,12 +1069,16 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
if (!this._compositionHelper!.keydown(event)) {
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this.scrollToBottom();
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = evaluateKeyboardEvent(event, this._coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
|
||||
if (event.key === 'Dead' || event.key === 'AltGraph') {
|
||||
this._unprocessedDeadKey = true;
|
||||
}
|
||||
|
||||
const result = evaluateKeyboardEvent(event, this.coreService.decPrivateModes.applicationCursorKeys, this.browser.isMac, this.options.macOptionIsMeta);
|
||||
|
||||
this.updateCursorStyle(event);
|
||||
|
||||
@@ -1031,6 +1105,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this._unprocessedDeadKey) {
|
||||
this._unprocessedDeadKey = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If ctrl+c or enter is being sent, clear out the textarea. This is done so that screen readers
|
||||
// will announce deleted characters. This will not work 100% of the time but it should cover
|
||||
// most scenarios.
|
||||
@@ -1040,7 +1119,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._onKey.fire({ key: result.key, domEvent: event });
|
||||
this._showCursor();
|
||||
this._coreService.triggerDataEvent(result.key, true);
|
||||
this.coreService.triggerDataEvent(result.key, true);
|
||||
|
||||
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
||||
// other listeners. When screen reader mode is enabled, this could cause issues if the event
|
||||
@@ -1053,10 +1132,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._keyDownHandled = true;
|
||||
}
|
||||
|
||||
private _isThirdLevelShift(browser: IBrowser, ev: IKeyboardEvent): boolean {
|
||||
private _isThirdLevelShift(browser: IBrowser, ev: KeyboardEvent): boolean {
|
||||
const thirdLevelKey =
|
||||
(browser.isMac && !this.options.macOptionIsMeta && ev.altKey && !ev.ctrlKey && !ev.metaKey) ||
|
||||
(browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey);
|
||||
(browser.isWindows && ev.altKey && ev.ctrlKey && !ev.metaKey) ||
|
||||
(browser.isWindows && ev.getModifierState('AltGraph'));
|
||||
|
||||
if (ev.type === 'keypress') {
|
||||
return thirdLevelKey;
|
||||
@@ -1076,6 +1156,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
this.updateCursorStyle(ev);
|
||||
this._keyPressHandled = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1087,6 +1168,8 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
protected _keyPress(ev: KeyboardEvent): boolean {
|
||||
let key;
|
||||
|
||||
this._keyPressHandled = false;
|
||||
|
||||
if (this._keyDownHandled) {
|
||||
return false;
|
||||
}
|
||||
@@ -1117,20 +1200,56 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this._onKey.fire({ key, domEvent: ev });
|
||||
this._showCursor();
|
||||
this._coreService.triggerDataEvent(key, true);
|
||||
this.coreService.triggerDataEvent(key, true);
|
||||
|
||||
this._keyPressHandled = true;
|
||||
|
||||
// The key was handled so clear the dead key state, otherwise certain keystrokes like arrow
|
||||
// keys could be ignored
|
||||
this._unprocessedDeadKey = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an input event.
|
||||
* Key Resources:
|
||||
* - https://developer.mozilla.org/en-US/docs/Web/API/InputEvent
|
||||
* @param ev The input event to be handled.
|
||||
*/
|
||||
protected _inputEvent(ev: InputEvent): boolean {
|
||||
// Only support emoji IMEs when screen reader mode is disabled as the event must bubble up to
|
||||
// support reading out character input which can doubling up input characters
|
||||
if (ev.data && ev.inputType === 'insertText' && !ev.composed && !this.optionsService.options.screenReaderMode) {
|
||||
if (this._keyPressHandled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The key was handled so clear the dead key state, otherwise certain keystrokes like arrow
|
||||
// keys could be ignored
|
||||
this._unprocessedDeadKey = false;
|
||||
|
||||
const text = ev.data;
|
||||
this.coreService.triggerDataEvent(text, true);
|
||||
|
||||
this.cancel(ev);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ring the bell.
|
||||
* Note: We could do sweet things with webaudio here
|
||||
*/
|
||||
public bell(): void {
|
||||
if (this._soundBell()) {
|
||||
this._soundService!.playBellSound();
|
||||
this._soundService?.playBellSound();
|
||||
}
|
||||
|
||||
this._onBell.fire();
|
||||
|
||||
// if (this._visualBell()) {
|
||||
// this.element.classList.add('visual-bell-active');
|
||||
// clearTimeout(this._visualBellTimer);
|
||||
@@ -1183,7 +1302,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.buffer.lines.push(this.buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
this.refresh(0, this.rows - 1);
|
||||
this._onScroll.fire(this.buffer.ydisp);
|
||||
this._onScroll.fire({ position: this.buffer.ydisp, source: ScrollSource.TERMINAL });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1215,6 +1334,18 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.viewport?.syncScrollArea();
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._renderService?.clearTextureAtlas();
|
||||
}
|
||||
|
||||
private _reportFocus(): void {
|
||||
if (this.element?.classList.contains('focus')) {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[I');
|
||||
} else {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[O');
|
||||
}
|
||||
}
|
||||
|
||||
private _reportWindowsOptions(type: WindowsOptionsReportType): void {
|
||||
if (!this._renderService) {
|
||||
return;
|
||||
@@ -1224,12 +1355,12 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
|
||||
const canvasWidth = this._renderService.dimensions.scaledCanvasWidth.toFixed(0);
|
||||
const canvasHeight = this._renderService.dimensions.scaledCanvasHeight.toFixed(0);
|
||||
this._coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
||||
break;
|
||||
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
|
||||
const cellWidth = this._renderService.dimensions.scaledCellWidth.toFixed(0);
|
||||
const cellHeight = this._renderService.dimensions.scaledCellHeight.toFixed(0);
|
||||
this._coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const ROWS = 25;
|
||||
|
||||
const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '../..')});
|
||||
const SKIP_FILES = [
|
||||
't0055-EL.in', // EL/ED handle cursor at cols differently (see #3362)
|
||||
't0084-CBT.in',
|
||||
't0101-NLM.in',
|
||||
't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose
|
||||
@@ -73,8 +74,8 @@ describe('Escape Sequence Files', function(): void {
|
||||
// register handler to trigger viewport scraping, wait for it to finish
|
||||
let content = '';
|
||||
const OSC_CODE = 12345;
|
||||
await new Promise(resolve => {
|
||||
customHandler = term.addOscHandler(OSC_CODE, () => {
|
||||
await new Promise<void>(resolve => {
|
||||
customHandler = term.registerOscHandler(OSC_CODE, () => {
|
||||
// grab terminal viewport content
|
||||
content = terminalToString(term);
|
||||
resolve();
|
||||
@@ -105,7 +106,7 @@ function formatError(input: string, output: string, expected: string): string {
|
||||
function addLineNumber(start: number, color: string): (s: string) => string {
|
||||
let counter = start || 0;
|
||||
return (s: string): string => {
|
||||
counter += 1;
|
||||
counter++;
|
||||
return '\x1b[33m' + (' ' + counter).slice(-2) + color + s;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,15 +5,15 @@
|
||||
|
||||
import { IDisposable, IMarker, ISelectionPosition, ILinkProvider } from 'xterm';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, CharacterJoinerHandler, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { Terminal } from 'browser/Terminal';
|
||||
import { IUnicodeService, IOptionsService } from 'common/services/Services';
|
||||
import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } from 'common/services/Services';
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
@@ -21,6 +21,9 @@ export class TestTerminal extends Terminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
public writeP(data: string | Uint8Array): Promise<void> {
|
||||
return new Promise(r => this.write(data, r));
|
||||
}
|
||||
}
|
||||
|
||||
export class MockTerminal implements ITerminal {
|
||||
@@ -34,11 +37,14 @@ export class MockTerminal implements ITerminal {
|
||||
public onData!: IEvent<string>;
|
||||
public onBinary!: IEvent<string>;
|
||||
public onTitleChange!: IEvent<string>;
|
||||
public onBell!: IEvent<void>;
|
||||
public onScroll!: IEvent<number>;
|
||||
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
||||
public markers!: IMarker[];
|
||||
public coreMouseService!: ICoreMouseService;
|
||||
public coreService!: ICoreService;
|
||||
public optionsService!: IOptionsService;
|
||||
public unicodeService!: IUnicodeService;
|
||||
public addMarker(cursorYOffset: number): IMarker {
|
||||
@@ -75,16 +81,16 @@ export class MockTerminal implements ITerminal {
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
|
||||
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
|
||||
@@ -149,7 +155,7 @@ export class MockTerminal implements ITerminal {
|
||||
public textarea!: HTMLTextAreaElement;
|
||||
public rows!: number;
|
||||
public cols!: number;
|
||||
public browser: IBrowser = <any>Browser;
|
||||
public browser: IBrowser = Browser as any;
|
||||
public writeBuffer!: string[];
|
||||
public children!: HTMLElement[];
|
||||
public cursorHidden!: boolean;
|
||||
@@ -189,6 +195,9 @@ export class MockTerminal implements ITerminal {
|
||||
public reset(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearTextureAtlas(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public refresh(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -281,8 +290,6 @@ export class MockRenderer implements IRenderer {
|
||||
public onDevicePixelRatioChange(): void { }
|
||||
public clear(): void { }
|
||||
public renderRows(start: number, end: number): void { }
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number { return 0; }
|
||||
public deregisterCharacterJoiner(): boolean { return true; }
|
||||
}
|
||||
|
||||
export class MockViewport implements IViewport {
|
||||
@@ -370,6 +377,9 @@ export class MockRenderService implements IRenderService {
|
||||
public refreshRows(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clearTextureAtlas(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public resize(cols: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -406,13 +416,20 @@ export class MockRenderService implements IRenderService {
|
||||
public clear(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
export class MockCharacterJoinerService implements ICharacterJoinerService {
|
||||
public serviceBrand: undefined;
|
||||
public register(handler: (text: string) => [number, number][]): number {
|
||||
return 0;
|
||||
}
|
||||
public deregister(joinerId: number): boolean {
|
||||
return true;
|
||||
}
|
||||
public getJoinedCharacters(row: number): [number, number][] {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
||||
|
||||
import { IRenderDebouncer } from 'browser/Types';
|
||||
|
||||
/**
|
||||
* Debounces calls to update screen readers to update at most once configurable interval of time.
|
||||
*/
|
||||
export class TimeBasedDebouncer implements IRenderDebouncer {
|
||||
private _rowStart: number | undefined;
|
||||
private _rowEnd: number | undefined;
|
||||
private _rowCount: number | undefined;
|
||||
|
||||
// The last moment that the Terminal was refreshed at
|
||||
private _lastRefreshMs = 0;
|
||||
// Whether a trailing refresh should be triggered due to a refresh request that was throttled
|
||||
private _additionalRefreshRequested = false;
|
||||
|
||||
private _refreshTimeoutID: number | undefined;
|
||||
|
||||
constructor(
|
||||
private _renderCallback: (start: number, end: number) => void,
|
||||
private readonly _debounceThresholdMS = RENDER_DEBOUNCE_THRESHOLD_MS
|
||||
) {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._refreshTimeoutID) {
|
||||
clearTimeout(this._refreshTimeoutID);
|
||||
}
|
||||
}
|
||||
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
this._rowCount = rowCount;
|
||||
// Get the min/max row start/end for the arg values
|
||||
rowStart = rowStart !== undefined ? rowStart : 0;
|
||||
rowEnd = rowEnd !== undefined ? rowEnd : this._rowCount - 1;
|
||||
// Set the properties to the updated values
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
// Only refresh if the time since last refresh is above a threshold, otherwise wait for
|
||||
// enough time to pass before refreshing again.
|
||||
const refreshRequestTime: number = Date.now();
|
||||
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
|
||||
// Enough time has lapsed since the last refresh; refresh immediately
|
||||
this._lastRefreshMs = refreshRequestTime;
|
||||
this._innerRefresh();
|
||||
} else if (!this._additionalRefreshRequested) {
|
||||
// This is the first additional request throttled; set up trailing refresh
|
||||
const elapsed = refreshRequestTime - this._lastRefreshMs;
|
||||
const waitPeriodBeforeTrailingRefresh = this._debounceThresholdMS - elapsed;
|
||||
this._additionalRefreshRequested = true;
|
||||
|
||||
this._refreshTimeoutID = window.setTimeout(() => {
|
||||
this._lastRefreshMs = Date.now();
|
||||
this._innerRefresh();
|
||||
this._additionalRefreshRequested = false;
|
||||
this._refreshTimeoutID = undefined; // No longer need to clear the timeout
|
||||
}, waitPeriodBeforeTrailingRefresh);
|
||||
}
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
// Make sure values are set
|
||||
if (this._rowStart === undefined || this._rowEnd === undefined || this._rowCount === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clamp values
|
||||
const start = Math.max(this._rowStart, 0);
|
||||
const end = Math.min(this._rowEnd, this._rowCount - 1);
|
||||
|
||||
// Reset debouncer (this happens before render callback as the render could trigger it again)
|
||||
this._rowStart = undefined;
|
||||
this._rowEnd = undefined;
|
||||
|
||||
// Run render callback
|
||||
this._renderCallback(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+27
-6
@@ -15,9 +15,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal {
|
||||
screenElement: HTMLElement | undefined;
|
||||
browser: IBrowser;
|
||||
buffer: IBuffer;
|
||||
buffers: IBufferSet;
|
||||
viewport: IViewport | undefined;
|
||||
// TODO: We should remove options once components adopt optionsService
|
||||
options: ITerminalOptions;
|
||||
linkifier: ILinkifier;
|
||||
linkifier2: ILinkifier2;
|
||||
@@ -47,15 +45,16 @@ export interface IPublicTerminal extends IDisposable {
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
onBell: IEvent<void>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
|
||||
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
|
||||
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
|
||||
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
|
||||
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
@@ -79,6 +78,7 @@ export interface IPublicTerminal extends IDisposable {
|
||||
write(data: string | Uint8Array, callback?: () => void): void;
|
||||
paste(data: string): void;
|
||||
refresh(start: number, end: number): void;
|
||||
clearTextureAtlas(): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
@@ -205,9 +205,19 @@ export interface ILinkifier {
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
|
||||
interface ILinkState {
|
||||
decorations: ILinkDecorations;
|
||||
isHovered: boolean;
|
||||
}
|
||||
export interface ILinkWithState {
|
||||
link: ILink;
|
||||
state?: ILinkState;
|
||||
}
|
||||
|
||||
export interface ILinkifier2 {
|
||||
onShowLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
onHideLinkUnderline: IEvent<ILinkifierEvent>;
|
||||
readonly currentLink: ILinkWithState | undefined;
|
||||
|
||||
attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void;
|
||||
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
|
||||
@@ -292,3 +302,14 @@ interface IBufferCellPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
|
||||
export interface ICharacterJoiner {
|
||||
id: number;
|
||||
handler: CharacterJoinerHandler;
|
||||
}
|
||||
|
||||
export interface IRenderDebouncer extends IDisposable {
|
||||
refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void;
|
||||
}
|
||||
|
||||
+40
-13
@@ -8,6 +8,8 @@ import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IViewport } from 'browser/Types';
|
||||
import { ICharSizeService, IRenderService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
@@ -18,11 +20,15 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
export class Viewport extends Disposable implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
private _currentScaledCellHeight: number = 0;
|
||||
private _lastRecordedBufferLength: number = 0;
|
||||
private _lastRecordedViewportHeight: number = 0;
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number = 0;
|
||||
private _lastScrollTop: number = 0;
|
||||
private _lastHadScrollBar: boolean = false;
|
||||
private _activeBuffer: IBuffer;
|
||||
private _renderDimensions: IRenderDimensions;
|
||||
|
||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||
@@ -33,9 +39,10 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _ignoreNextScrollEvent: boolean = false;
|
||||
|
||||
constructor(
|
||||
private readonly _scrollLines: (amount: number, suppressEvent: boolean) => void,
|
||||
private readonly _scrollLines: (amount: number) => void,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _scrollArea: HTMLElement,
|
||||
private readonly _element: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@@ -47,8 +54,15 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this._lastHadScrollBar = true;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Track properties used in performance critical code manually to avoid using slow getters
|
||||
this._activeBuffer = this._bufferService.buffer;
|
||||
this.register(this._bufferService.buffers.onBufferActivate(e => this._activeBuffer = e.activeBuffer));
|
||||
this._renderDimensions = this._renderService.dimensions;
|
||||
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
|
||||
|
||||
// Perform this async to ensure the ICharSizeService is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
@@ -77,6 +91,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private _innerRefresh(): void {
|
||||
if (this._charSizeService.height > 0) {
|
||||
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
|
||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
@@ -94,8 +109,20 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
// Update scroll bar width
|
||||
if (this._optionsService.options.scrollback === 0) {
|
||||
this.scrollBarWidth = 0;
|
||||
} else {
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
}
|
||||
this._lastHadScrollBar = this.scrollBarWidth > 0;
|
||||
|
||||
const elementStyle = window.getComputedStyle(this._element);
|
||||
const elementPadding = parseInt(elementStyle.paddingLeft) + parseInt(elementStyle.paddingRight);
|
||||
this._viewportElement.style.width = (this._renderService.dimensions.actualCellWidth * (this._bufferService.cols) + this.scrollBarWidth + (this._lastHadScrollBar ? elementPadding : 0)).toString() + 'px';
|
||||
this._refreshAnimationFrame = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||
*/
|
||||
@@ -114,23 +141,21 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
// If the buffer position doesn't match last scroll top
|
||||
const newScrollTop = this._bufferService.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._lastScrollTop !== newScrollTop) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If element's scroll top changed, this can happen when hiding the element
|
||||
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
|
||||
if (this._lastScrollTop !== this._activeBuffer.ydisp * this._currentRowHeight) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If row height changed
|
||||
if (this._renderService.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the scroll bar visibility changed
|
||||
if (this._lastHadScrollBar !== (this._optionsService.options.scrollback > 0)) {
|
||||
this._refresh(immediate);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,12 +176,14 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
||||
if (this._ignoreNextScrollEvent) {
|
||||
this._ignoreNextScrollEvent = false;
|
||||
// Still trigger the scroll so lines get refreshed
|
||||
this._scrollLines(0);
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this._lastScrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._bufferService.buffer.ydisp;
|
||||
this._scrollLines(diff, true);
|
||||
this._scrollLines(diff);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,7 +220,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
|
||||
private _getPixelsScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
if (ev.deltaY === 0 || ev.shiftKey) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -214,7 +241,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
*/
|
||||
public getLinesScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
if (ev.deltaY === 0 || ev.shiftKey) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { MockCharSizeService } from 'browser/TestUtils.test';
|
||||
import { MockRenderService } from 'browser/TestUtils.test';
|
||||
import { MockCoreService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
|
||||
describe('CompositionHelper', () => {
|
||||
@@ -42,14 +42,14 @@ describe('CompositionHelper', () => {
|
||||
};
|
||||
handledText = '';
|
||||
const bufferService = new MockBufferService(10, 5);
|
||||
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), new MockCharSizeService(10, 10), coreService);
|
||||
compositionHelper = new CompositionHelper(textarea, compositionView, bufferService, new MockOptionsService(), coreService, new MockRenderService());
|
||||
});
|
||||
|
||||
describe('Input', () => {
|
||||
it('Should insert simple characters', (done) => {
|
||||
// First character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -57,7 +57,7 @@ describe('CompositionHelper', () => {
|
||||
assert.equal(handledText, 'ㅇ');
|
||||
// Second character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -73,13 +73,13 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert complex characters', (done) => {
|
||||
// First character '앙'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
textarea.value = '아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
textarea.value = '앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -87,13 +87,13 @@ describe('CompositionHelper', () => {
|
||||
assert.equal(handledText, '앙');
|
||||
// Second character '앙'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = '앙ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
textarea.value = '앙아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
textarea.value = '앙앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -113,19 +113,19 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert complex characters that change with following character', (done) => {
|
||||
// First character '아'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
textarea.value = '아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Start second character '아' in first character
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
textarea.value = '앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
textarea.value = '아아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -142,14 +142,14 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-characters compositions', (done) => {
|
||||
// First character 'だ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
|
||||
compositionHelper.compositionupdate({ data: 'd' });
|
||||
textarea.value = 'd';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
|
||||
compositionHelper.compositionupdate({ data: 'だ' });
|
||||
textarea.value = 'だ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'あ'
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だあ' });
|
||||
compositionHelper.compositionupdate({ data: 'だあ' });
|
||||
textarea.value = 'だあ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -165,18 +165,18 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => {
|
||||
// First character 'だ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
|
||||
compositionHelper.compositionupdate({ data: 'd' });
|
||||
textarea.value = 'd';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
|
||||
compositionHelper.compositionupdate({ data: 'だ' });
|
||||
textarea.value = 'だ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'ー'
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だー' });
|
||||
compositionHelper.compositionupdate({ data: 'だー' });
|
||||
textarea.value = 'だー';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Convert to katakana 'ダー'
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ダー' });
|
||||
compositionHelper.compositionupdate({ data: 'ダー' });
|
||||
textarea.value = 'ダー';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -193,18 +193,18 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => {
|
||||
// First character 'い'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'い' });
|
||||
compositionHelper.compositionupdate({ data: 'い' });
|
||||
textarea.value = 'い';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'ま'
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いm' });
|
||||
compositionHelper.compositionupdate({ data: 'いm' });
|
||||
textarea.value = 'いm';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いま' });
|
||||
compositionHelper.compositionupdate({ data: 'いま' });
|
||||
textarea.value = 'いま';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Convert to kanji '今'
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '今' });
|
||||
compositionHelper.compositionupdate({ data: '今' });
|
||||
textarea.value = '今';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -221,7 +221,7 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert non-composition characters input immediately after composition characters', (done) => {
|
||||
// First character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { IBufferService, ICoreService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
interface IPosition {
|
||||
@@ -45,8 +45,8 @@ export class CompositionHelper {
|
||||
private readonly _compositionView: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@ICoreService private readonly _coreService: ICoreService
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IRenderService private readonly _renderService: IRenderService
|
||||
) {
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
@@ -69,7 +69,7 @@ export class CompositionHelper {
|
||||
* Handles the compositionupdate event, updating the composition view.
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
public compositionupdate(ev: Pick<CompositionEvent, 'data'>): void {
|
||||
this._compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
@@ -207,9 +207,11 @@ export class CompositionHelper {
|
||||
}
|
||||
|
||||
if (this._bufferService.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this._charSizeService.height * this._optionsService.options.lineHeight);
|
||||
const cursorTop = this._bufferService.buffer.y * cellHeight;
|
||||
const cursorLeft = this._bufferService.buffer.x * this._charSizeService.width;
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
@@ -222,8 +224,9 @@ export class CompositionHelper {
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
this._textarea.style.left = cursorLeft + 'px';
|
||||
this._textarea.style.top = cursorTop + 'px';
|
||||
this._textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this._textarea.style.height = compositionViewBounds.height + 'px';
|
||||
// Ensure the text area is at least 1x1, otherwise certain IMEs may break
|
||||
this._textarea.style.width = Math.max(compositionViewBounds.width, 1) + 'px';
|
||||
this._textarea.style.height = Math.max(compositionViewBounds.height, 1) + 'px';
|
||||
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
}
|
||||
|
||||
|
||||
@@ -121,7 +121,7 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
|
||||
const line = bufferService.buffer.lines.get(startRow + (direction * i));
|
||||
if (line && line.isWrapped) {
|
||||
if (line?.isWrapped) {
|
||||
wrappedRows++;
|
||||
}
|
||||
}
|
||||
@@ -136,12 +136,12 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
|
||||
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
|
||||
let rowCount = 0;
|
||||
let line = bufferService.buffer.lines.get(currentRow);
|
||||
let lineWraps = line && line.isWrapped;
|
||||
let lineWraps = line?.isWrapped;
|
||||
|
||||
while (lineWraps && currentRow >= 0 && currentRow < bufferService.rows) {
|
||||
rowCount++;
|
||||
line = bufferService.buffer.lines.get(--currentRow);
|
||||
lineWraps = line && line.isWrapped;
|
||||
lineWraps = line?.isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
|
||||
+77
-136
@@ -3,27 +3,54 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferNamespace as IBufferNamespaceApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, IParser, IFunctionIdentifier, ILinkProvider, IUnicodeHandling, IUnicodeVersionProvider, FontWeight } from 'xterm';
|
||||
import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes } from 'xterm';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { IBufferLine, ICellData } from 'common/Types';
|
||||
import { IBuffer, IBufferSet } from 'common/buffer/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Terminal as TerminalCore } from '../Terminal';
|
||||
import * as Strings from '../LocalizableStrings';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { AddonManager } from './AddonManager';
|
||||
import { IParams } from 'common/parser/Types';
|
||||
import { BufferSet } from 'common/buffer/BufferSet';
|
||||
import { Terminal as TerminalCore } from 'browser/Terminal';
|
||||
import * as Strings from 'browser/LocalizableStrings';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { ParserApi } from 'common/public/ParserApi';
|
||||
import { UnicodeApi } from 'common/public/UnicodeApi';
|
||||
import { AddonManager } from 'common/public/AddonManager';
|
||||
import { BufferNamespaceApi } from 'common/public/BufferNamespaceApi';
|
||||
import { ITerminalOptions } from 'common/Types';
|
||||
|
||||
/**
|
||||
* The set of options that only have an effect when set in the Terminal constructor.
|
||||
*/
|
||||
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
|
||||
|
||||
export class Terminal implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
private _addonManager: AddonManager;
|
||||
private _parser: IParser | undefined;
|
||||
private _buffer: BufferNamespaceApi | undefined;
|
||||
private _publicOptions: ITerminalOptions;
|
||||
|
||||
constructor(options?: ITerminalOptions) {
|
||||
this._core = new TerminalCore(options);
|
||||
this._addonManager = new AddonManager();
|
||||
|
||||
this._publicOptions = {};
|
||||
for (const propName in this._core.options) {
|
||||
Object.defineProperty(this._publicOptions, propName, {
|
||||
get: () => {
|
||||
return this._core.options[propName];
|
||||
},
|
||||
set: (value: any) => {
|
||||
this._checkReadonlyOptions(propName);
|
||||
this._core.options[propName] = value;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _checkReadonlyOptions(propName: string): void {
|
||||
// Throw an error if any constructor only option is modified
|
||||
// from terminal.options
|
||||
// Modifications from anywhere else are allowed
|
||||
if (CONSTRUCTOR_ONLY_OPTIONS.includes(propName)) {
|
||||
throw new Error(`Option "${propName}" can only be set in the constructor`);
|
||||
}
|
||||
}
|
||||
|
||||
private _checkProposedApi(): void {
|
||||
@@ -32,16 +59,17 @@ export class Terminal implements ITerminalApi {
|
||||
}
|
||||
}
|
||||
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onBell(): IEvent<void> { return this._core.onBell; }
|
||||
public get onBinary(): IEvent<string> { return this._core.onBinary; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
public get onData(): IEvent<string> { return this._core.onData; }
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._core.onKey; }
|
||||
public get onLineFeed(): IEvent<void> { return this._core.onLineFeed; }
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._core.onRender; }
|
||||
public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
|
||||
public get onScroll(): IEvent<number> { return this._core.onScroll; }
|
||||
public get onSelectionChange(): IEvent<void> { return this._core.onSelectionChange; }
|
||||
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
@@ -69,6 +97,35 @@ export class Terminal implements ITerminalApi {
|
||||
this._checkProposedApi();
|
||||
return this._core.markers;
|
||||
}
|
||||
public get modes(): IModes {
|
||||
const m = this._core.coreService.decPrivateModes;
|
||||
let mouseTrackingMode: 'none' | 'x10' | 'vt200' | 'drag' | 'any' = 'none';
|
||||
switch (this._core.coreMouseService.activeProtocol) {
|
||||
case 'X10': mouseTrackingMode = 'x10'; break;
|
||||
case 'VT200': mouseTrackingMode = 'vt200'; break;
|
||||
case 'DRAG': mouseTrackingMode = 'drag'; break;
|
||||
case 'ANY': mouseTrackingMode = 'any'; break;
|
||||
}
|
||||
return {
|
||||
applicationCursorKeysMode: m.applicationCursorKeys,
|
||||
applicationKeypadMode: m.applicationKeypad,
|
||||
bracketedPasteMode: m.bracketedPasteMode,
|
||||
insertMode: this._core.coreService.modes.insertMode,
|
||||
mouseTrackingMode: mouseTrackingMode,
|
||||
originMode: m.origin,
|
||||
reverseWraparoundMode: m.reverseWraparound,
|
||||
sendFocusMode: m.sendFocus,
|
||||
wraparoundMode: m.wraparound
|
||||
};
|
||||
}
|
||||
public get options(): ITerminalOptions {
|
||||
return this._publicOptions;
|
||||
}
|
||||
public set options(options: ITerminalOptions) {
|
||||
for (const propName in options) {
|
||||
this._publicOptions[propName] = options[propName];
|
||||
}
|
||||
}
|
||||
public blur(): void {
|
||||
this._core.blur();
|
||||
}
|
||||
@@ -193,6 +250,7 @@ export class Terminal implements ITerminalApi {
|
||||
public setOption(key: 'cols' | 'rows', value: number): void;
|
||||
public setOption(key: string, value: any): void;
|
||||
public setOption(key: any, value: any): void {
|
||||
this._checkReadonlyOptions(key);
|
||||
this._core.optionsService.setOption(key, value);
|
||||
}
|
||||
public refresh(start: number, end: number): void {
|
||||
@@ -202,6 +260,9 @@ export class Terminal implements ITerminalApi {
|
||||
public reset(): void {
|
||||
this._core.reset();
|
||||
}
|
||||
public clearTextureAtlas(): void {
|
||||
this._core.clearTextureAtlas();
|
||||
}
|
||||
public loadAddon(addon: ITerminalAddon): void {
|
||||
return this._addonManager.loadAddon(this, addon);
|
||||
}
|
||||
@@ -217,123 +278,3 @@ export class Terminal implements ITerminalApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BufferApiView implements IBufferApi {
|
||||
constructor(
|
||||
private _buffer: IBuffer,
|
||||
public readonly type: 'normal' | 'alternate'
|
||||
) { }
|
||||
|
||||
public init(buffer: IBuffer): BufferApiView {
|
||||
this._buffer = buffer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public get cursorY(): number { return this._buffer.y; }
|
||||
public get cursorX(): number { return this._buffer.x; }
|
||||
public get viewportY(): number { return this._buffer.ydisp; }
|
||||
public get baseY(): number { return this._buffer.ybase; }
|
||||
public get length(): number { return this._buffer.lines.length; }
|
||||
public getLine(y: number): IBufferLineApi | undefined {
|
||||
const line = this._buffer.lines.get(y);
|
||||
if (!line) {
|
||||
return undefined;
|
||||
}
|
||||
return new BufferLineApiView(line);
|
||||
}
|
||||
public getNullCell(): IBufferCellApi { return new CellData(); }
|
||||
}
|
||||
|
||||
class BufferNamespaceApi implements IBufferNamespaceApi {
|
||||
private _normal: BufferApiView;
|
||||
private _alternate: BufferApiView;
|
||||
private _onBufferChange = new EventEmitter<IBufferApi>();
|
||||
public get onBufferChange(): IEvent<IBufferApi> { return this._onBufferChange.event; }
|
||||
|
||||
constructor(private _core: ITerminal) {
|
||||
this._normal = new BufferApiView(this._core.buffers.normal, 'normal');
|
||||
this._alternate = new BufferApiView(this._core.buffers.alt, 'alternate');
|
||||
this._core.buffers.onBufferActivate(() => this._onBufferChange.fire(this.active));
|
||||
}
|
||||
public get active(): IBufferApi {
|
||||
if (this._core.buffers.active === this._core.buffers.normal) { return this.normal; }
|
||||
if (this._core.buffers.active === this._core.buffers.alt) { return this.alternate; }
|
||||
throw new Error('Active buffer is neither normal nor alternate');
|
||||
}
|
||||
public get normal(): IBufferApi {
|
||||
return this._normal.init(this._core.buffers.normal);
|
||||
}
|
||||
public get alternate(): IBufferApi {
|
||||
return this._alternate.init(this._core.buffers.alt);
|
||||
}
|
||||
}
|
||||
|
||||
class BufferLineApiView implements IBufferLineApi {
|
||||
constructor(private _line: IBufferLine) { }
|
||||
|
||||
public get isWrapped(): boolean { return this._line.isWrapped; }
|
||||
public get length(): number { return this._line.length; }
|
||||
public getCell(x: number, cell?: IBufferCellApi): IBufferCellApi | undefined {
|
||||
if (x < 0 || x >= this._line.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (cell) {
|
||||
this._line.loadCell(x, <ICellData>cell);
|
||||
return cell;
|
||||
}
|
||||
return this._line.loadCell(x, new CellData());
|
||||
}
|
||||
public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
|
||||
return this._line.translateToString(trimRight, startColumn, endColumn);
|
||||
}
|
||||
}
|
||||
|
||||
class ParserApi implements IParser {
|
||||
constructor(private _core: ITerminal) { }
|
||||
|
||||
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
|
||||
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
|
||||
}
|
||||
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
|
||||
return this.registerCsiHandler(id, callback);
|
||||
}
|
||||
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
|
||||
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
|
||||
}
|
||||
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
|
||||
return this.registerDcsHandler(id, callback);
|
||||
}
|
||||
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
return this._core.addEscHandler(id, handler);
|
||||
}
|
||||
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
|
||||
return this.registerEscHandler(id, handler);
|
||||
}
|
||||
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this._core.addOscHandler(ident, callback);
|
||||
}
|
||||
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
|
||||
return this.registerOscHandler(ident, callback);
|
||||
}
|
||||
}
|
||||
|
||||
class UnicodeApi implements IUnicodeHandling {
|
||||
constructor(private _core: ITerminal) { }
|
||||
|
||||
public register(provider: IUnicodeVersionProvider): void {
|
||||
this._core.unicodeService.register(provider);
|
||||
}
|
||||
|
||||
public get versions(): string[] {
|
||||
return this._core.unicodeService.versions;
|
||||
}
|
||||
|
||||
public get activeVersion(): string {
|
||||
return this._core.unicodeService.activeVersion;
|
||||
}
|
||||
|
||||
public set activeVersion(version: string) {
|
||||
this._core.unicodeService.activeVersion = version;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
|
||||
import { IGlyphIdentifier } from 'browser/renderer/atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
@@ -17,6 +17,7 @@ import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { channels, color, rgba } from 'browser/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -66,7 +67,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();
|
||||
@@ -137,6 +138,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
public abstract reset(): void;
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._charAtlas?.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||
* @param x The column to start at.
|
||||
@@ -152,6 +157,21 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
height * this._scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - cellOffset - window.devicePixelRatio,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
@@ -242,12 +262,22 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
this._clipRow(y);
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.options.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -295,12 +325,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._currentGlyphIdentifier.bold = !!cell.isBold();
|
||||
this._currentGlyphIdentifier.dim = !!cell.isDim();
|
||||
this._currentGlyphIdentifier.italic = !!cell.isItalic();
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
this._currentGlyphIdentifier,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
const atlasDidDraw = this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
@@ -320,7 +345,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.textBaseline = 'middle';
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (fgOverride) {
|
||||
@@ -358,14 +383,25 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (cell.isDim()) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.options.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
||||
if (!drawSuccess) {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param y The row to clip.
|
||||
|
||||
@@ -37,10 +37,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _coreService: ICoreService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
this._state = {
|
||||
@@ -55,7 +55,14 @@ 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?
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -72,26 +79,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
public reset(): void {
|
||||
this._clearCursor();
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
this.onOptionsChanged();
|
||||
}
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
this.onOptionsChanged();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.pause();
|
||||
}
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.resume();
|
||||
} else {
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
this._cursorBlinkStateManager?.resume();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
@@ -111,9 +110,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.restartBlinkAnimation();
|
||||
}
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
}
|
||||
|
||||
public onGridChanged(startRow: number, endRow: number): void {
|
||||
@@ -197,7 +194,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
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,
|
||||
@@ -300,6 +302,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
|
||||
|
||||
@@ -0,0 +1,563 @@
|
||||
/**
|
||||
* Copyright (c) 2021 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
|
||||
interface IBlockVector {
|
||||
x: number;
|
||||
y: number;
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefined } = {
|
||||
// Block elements (0x2580-0x2590)
|
||||
'▀': [{ x: 0, y: 0, w: 8, h: 4 }], // UPPER HALF BLOCK
|
||||
'▁': [{ x: 0, y: 7, w: 8, h: 1 }], // LOWER ONE EIGHTH BLOCK
|
||||
'▂': [{ x: 0, y: 6, w: 8, h: 2 }], // LOWER ONE QUARTER BLOCK
|
||||
'▃': [{ x: 0, y: 5, w: 8, h: 3 }], // LOWER THREE EIGHTHS BLOCK
|
||||
'▄': [{ x: 0, y: 4, w: 8, h: 4 }], // LOWER HALF BLOCK
|
||||
'▅': [{ x: 0, y: 3, w: 8, h: 5 }], // LOWER FIVE EIGHTHS BLOCK
|
||||
'▆': [{ x: 0, y: 2, w: 8, h: 6 }], // LOWER THREE QUARTERS BLOCK
|
||||
'▇': [{ x: 0, y: 1, w: 8, h: 7 }], // LOWER SEVEN EIGHTHS BLOCK
|
||||
'█': [{ x: 0, y: 0, w: 8, h: 8 }], // FULL BLOCK
|
||||
'▉': [{ x: 0, y: 0, w: 7, h: 8 }], // LEFT SEVEN EIGHTHS BLOCK
|
||||
'▊': [{ x: 0, y: 0, w: 6, h: 8 }], // LEFT THREE QUARTERS BLOCK
|
||||
'▋': [{ x: 0, y: 0, w: 5, h: 8 }], // LEFT FIVE EIGHTHS BLOCK
|
||||
'▌': [{ x: 0, y: 0, w: 4, h: 8 }], // LEFT HALF BLOCK
|
||||
'▍': [{ x: 0, y: 0, w: 3, h: 8 }], // LEFT THREE EIGHTHS BLOCK
|
||||
'▎': [{ x: 0, y: 0, w: 2, h: 8 }], // LEFT ONE QUARTER BLOCK
|
||||
'▏': [{ x: 0, y: 0, w: 1, h: 8 }], // LEFT ONE EIGHTH BLOCK
|
||||
'▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK
|
||||
|
||||
// Block elements (0x2594-0x2595)
|
||||
'▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK
|
||||
'▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK
|
||||
|
||||
// Terminal graphic characters (0x2596-0x259F)
|
||||
'▖': [{ x: 0, y: 4, w: 4, h: 4 }], // QUADRANT LOWER LEFT
|
||||
'▗': [{ x: 4, y: 4, w: 4, h: 4 }], // QUADRANT LOWER RIGHT
|
||||
'▘': [{ x: 0, y: 0, w: 4, h: 4 }], // QUADRANT UPPER LEFT
|
||||
'▙': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER LEFT AND LOWER LEFT AND LOWER RIGHT
|
||||
'▚': [{ x: 0, y: 0, w: 4, h: 4 }, { x: 4, y: 4, w: 4, h: 4 }], // QUADRANT UPPER LEFT AND LOWER RIGHT
|
||||
'▛': [{ x: 0, y: 0, w: 4, h: 8 }, { x: 0, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER LEFT
|
||||
'▜': [{ x: 0, y: 0, w: 8, h: 4 }, { x: 4, y: 0, w: 4, h: 8 }], // QUADRANT UPPER LEFT AND UPPER RIGHT AND LOWER RIGHT
|
||||
'▝': [{ x: 4, y: 0, w: 4, h: 4 }], // QUADRANT UPPER RIGHT
|
||||
'▞': [{ x: 4, y: 0, w: 4, h: 4 }, { x: 0, y: 4, w: 4, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT
|
||||
'▟': [{ x: 4, y: 0, w: 4, h: 8 }, { x: 0, y: 4, w: 8, h: 4 }], // QUADRANT UPPER RIGHT AND LOWER LEFT AND LOWER RIGHT
|
||||
|
||||
// VERTICAL ONE EIGHTH BLOCK-2 through VERTICAL ONE EIGHTH BLOCK-7
|
||||
'\u{1FB70}': [{ x: 1, y: 0, w: 1, h: 8 }],
|
||||
'\u{1FB71}': [{ x: 2, y: 0, w: 1, h: 8 }],
|
||||
'\u{1FB72}': [{ x: 3, y: 0, w: 1, h: 8 }],
|
||||
'\u{1FB73}': [{ x: 4, y: 0, w: 1, h: 8 }],
|
||||
'\u{1FB74}': [{ x: 5, y: 0, w: 1, h: 8 }],
|
||||
'\u{1FB75}': [{ x: 6, y: 0, w: 1, h: 8 }],
|
||||
|
||||
// HORIZONTAL ONE EIGHTH BLOCK-2 through HORIZONTAL ONE EIGHTH BLOCK-7
|
||||
'\u{1FB76}': [{ x: 0, y: 1, w: 8, h: 1 }],
|
||||
'\u{1FB77}': [{ x: 0, y: 2, w: 8, h: 1 }],
|
||||
'\u{1FB78}': [{ x: 0, y: 3, w: 8, h: 1 }],
|
||||
'\u{1FB79}': [{ x: 0, y: 4, w: 8, h: 1 }],
|
||||
'\u{1FB7A}': [{ x: 0, y: 5, w: 8, h: 1 }],
|
||||
'\u{1FB7B}': [{ x: 0, y: 6, w: 8, h: 1 }],
|
||||
|
||||
// LEFT AND LOWER ONE EIGHTH BLOCK
|
||||
'\u{1FB7C}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
|
||||
// LEFT AND UPPER ONE EIGHTH BLOCK
|
||||
'\u{1FB7D}': [{ x: 0, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
|
||||
// RIGHT AND UPPER ONE EIGHTH BLOCK
|
||||
'\u{1FB7E}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 0, w: 8, h: 1 }],
|
||||
// RIGHT AND LOWER ONE EIGHTH BLOCK
|
||||
'\u{1FB7F}': [{ x: 7, y: 0, w: 1, h: 8 }, { x: 0, y: 7, w: 8, h: 1 }],
|
||||
// UPPER AND LOWER ONE EIGHTH BLOCK
|
||||
'\u{1FB80}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
|
||||
// HORIZONTAL ONE EIGHTH BLOCK-1358
|
||||
'\u{1FB81}': [{ x: 0, y: 0, w: 8, h: 1 }, { x: 0, y: 2, w: 8, h: 1 }, { x: 0, y: 4, w: 8, h: 1 }, { x: 0, y: 7, w: 8, h: 1 }],
|
||||
|
||||
// UPPER ONE QUARTER BLOCK
|
||||
'\u{1FB82}': [{ x: 0, y: 0, w: 8, h: 2 }],
|
||||
// UPPER THREE EIGHTHS BLOCK
|
||||
'\u{1FB83}': [{ x: 0, y: 0, w: 8, h: 3 }],
|
||||
// UPPER FIVE EIGHTHS BLOCK
|
||||
'\u{1FB84}': [{ x: 0, y: 0, w: 8, h: 5 }],
|
||||
// UPPER THREE QUARTERS BLOCK
|
||||
'\u{1FB85}': [{ x: 0, y: 0, w: 8, h: 6 }],
|
||||
// UPPER SEVEN EIGHTHS BLOCK
|
||||
'\u{1FB86}': [{ x: 0, y: 0, w: 8, h: 7 }],
|
||||
|
||||
// RIGHT ONE QUARTER BLOCK
|
||||
'\u{1FB87}': [{ x: 6, y: 0, w: 2, h: 8 }],
|
||||
// RIGHT THREE EIGHTHS B0OCK
|
||||
'\u{1FB88}': [{ x: 5, y: 0, w: 3, h: 8 }],
|
||||
// RIGHT FIVE EIGHTHS BL0CK
|
||||
'\u{1FB89}': [{ x: 3, y: 0, w: 5, h: 8 }],
|
||||
// RIGHT THREE QUARTERS 0LOCK
|
||||
'\u{1FB8A}': [{ x: 2, y: 0, w: 6, h: 8 }],
|
||||
// RIGHT SEVEN EIGHTHS B0OCK
|
||||
'\u{1FB8B}': [{ x: 1, y: 0, w: 7, h: 8 }],
|
||||
|
||||
// CHECKER BOARD FILL
|
||||
'\u{1FB95}': [
|
||||
{ x: 0, y: 0, w: 2, h: 2 }, { x: 4, y: 0, w: 2, h: 2 },
|
||||
{ x: 2, y: 2, w: 2, h: 2 }, { x: 6, y: 2, w: 2, h: 2 },
|
||||
{ x: 0, y: 4, w: 2, h: 2 }, { x: 4, y: 4, w: 2, h: 2 },
|
||||
{ x: 2, y: 6, w: 2, h: 2 }, { x: 6, y: 6, w: 2, h: 2 }
|
||||
],
|
||||
// INVERSE CHECKER BOARD FILL
|
||||
'\u{1FB96}': [
|
||||
{ x: 2, y: 0, w: 2, h: 2 }, { x: 6, y: 0, w: 2, h: 2 },
|
||||
{ x: 0, y: 2, w: 2, h: 2 }, { x: 4, y: 2, w: 2, h: 2 },
|
||||
{ x: 2, y: 4, w: 2, h: 2 }, { x: 6, y: 4, w: 2, h: 2 },
|
||||
{ x: 0, y: 6, w: 2, h: 2 }, { x: 4, y: 6, w: 2, h: 2 }
|
||||
],
|
||||
// HEAVY HORIZONTAL FILL (upper middle and lower one quarter block)
|
||||
'\u{1FB97}': [{ x: 0, y: 2, w: 8, h: 2 }, { x: 0, y: 6, w: 8, h: 2 }]
|
||||
};
|
||||
|
||||
type PatternDefinition = number[][];
|
||||
|
||||
/**
|
||||
* Defines the repeating pattern used by special characters, the pattern is made up of a 2d array of
|
||||
* pixel values to be filled (1) or not filled (0).
|
||||
*/
|
||||
const patternCharacterDefinitions: { [key: string]: PatternDefinition | undefined } = {
|
||||
// Shade characters (0x2591-0x2593)
|
||||
'░': [ // LIGHT SHADE (25%)
|
||||
[1, 0, 0, 0],
|
||||
[0, 0, 0, 0],
|
||||
[0, 0, 1, 0],
|
||||
[0, 0, 0, 0]
|
||||
],
|
||||
'▒': [ // MEDIUM SHADE (50%)
|
||||
[1, 0],
|
||||
[0, 0],
|
||||
[0, 1],
|
||||
[0, 0]
|
||||
],
|
||||
'▓': [ // DARK SHADE (75%)
|
||||
[0, 1],
|
||||
[1, 1],
|
||||
[1, 0],
|
||||
[1, 1]
|
||||
]
|
||||
};
|
||||
|
||||
const enum Shapes {
|
||||
/** │ */ TOP_TO_BOTTOM = 'M.5,0 L.5,1',
|
||||
/** ─ */ LEFT_TO_RIGHT = 'M0,.5 L1,.5',
|
||||
|
||||
/** └ */ TOP_TO_RIGHT = 'M.5,0 L.5,.5 L1,.5',
|
||||
/** ┘ */ TOP_TO_LEFT = 'M.5,0 L.5,.5 L0,.5',
|
||||
/** ┐ */ LEFT_TO_BOTTOM = 'M0,.5 L.5,.5 L.5,1',
|
||||
/** ┌ */ RIGHT_TO_BOTTOM = 'M0.5,1 L.5,.5 L1,.5',
|
||||
|
||||
/** ╵ */ MIDDLE_TO_TOP = 'M.5,.5 L.5,0',
|
||||
/** ╴ */ MIDDLE_TO_LEFT = 'M.5,.5 L0,.5',
|
||||
/** ╶ */ MIDDLE_TO_RIGHT = 'M.5,.5 L1,.5',
|
||||
/** ╷ */ MIDDLE_TO_BOTTOM = 'M.5,.5 L.5,1',
|
||||
|
||||
/** ┴ */ T_TOP = 'M0,.5 L1,.5 M.5,.5 L.5,0',
|
||||
/** ┤ */ T_LEFT = 'M.5,0 L.5,1 M.5,.5 L0,.5',
|
||||
/** ├ */ T_RIGHT = 'M.5,0 L.5,1 M.5,.5 L1,.5',
|
||||
/** ┬ */ T_BOTTOM = 'M0,.5 L1,.5 M.5,.5 L.5,1',
|
||||
|
||||
/** ┼ */ CROSS = 'M0,.5 L1,.5 M.5,0 L.5,1',
|
||||
|
||||
/** ╌ */ TWO_DASHES_HORIZONTAL = 'M.1,.5 L.4,.5 M.6,.5 L.9,.5', // .2 empty, .3 filled
|
||||
/** ┄ */ THREE_DASHES_HORIZONTAL = 'M.0667,.5 L.2667,.5 M.4,.5 L.6,.5 M.7333,.5 L.9333,.5', // .1333 empty, .2 filled
|
||||
/** ┉ */ FOUR_DASHES_HORIZONTAL = 'M.05,.5 L.2,.5 M.3,.5 L.45,.5 M.55,.5 L.7,.5 M.8,.5 L.95,.5', // .1 empty, .15 filled
|
||||
/** ╎ */ TWO_DASHES_VERTICAL = 'M.5,.1 L.5,.4 M.5,.6 L.5,.9',
|
||||
/** ┆ */ THREE_DASHES_VERTICAL = 'M.5,.0667 L.5,.2667 M.5,.4 L.5,.6 M.5,.7333 L.5,.9333',
|
||||
/** ┊ */ FOUR_DASHES_VERTICAL = 'M.5,.05 L.5,.2 M.5,.3 L.5,.45 L.5,.55 M.5,.7 L.5,.95',
|
||||
}
|
||||
|
||||
const enum Style {
|
||||
NORMAL = 1,
|
||||
BOLD = 3
|
||||
}
|
||||
|
||||
/**
|
||||
* This contains the definitions of all box drawing characters in the format of SVG paths (ie. the
|
||||
* svg d attribute).
|
||||
*/
|
||||
export const boxDrawingDefinitions: { [character: string]: { [fontWeight: number]: string | ((xp: number, yp: number) => string) } | undefined } = {
|
||||
// Uniform normal and bold
|
||||
'─': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT },
|
||||
'━': { [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
|
||||
'│': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM },
|
||||
'┃': { [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
|
||||
'┌': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM },
|
||||
'┏': { [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
|
||||
'┐': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM },
|
||||
'┓': { [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
|
||||
'└': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT },
|
||||
'┗': { [Style.BOLD]: Shapes.TOP_TO_RIGHT },
|
||||
'┘': { [Style.NORMAL]: Shapes.TOP_TO_LEFT },
|
||||
'┛': { [Style.BOLD]: Shapes.TOP_TO_LEFT },
|
||||
'├': { [Style.NORMAL]: Shapes.T_RIGHT },
|
||||
'┣': { [Style.BOLD]: Shapes.T_RIGHT },
|
||||
'┤': { [Style.NORMAL]: Shapes.T_LEFT },
|
||||
'┫': { [Style.BOLD]: Shapes.T_LEFT },
|
||||
'┬': { [Style.NORMAL]: Shapes.T_BOTTOM },
|
||||
'┳': { [Style.BOLD]: Shapes.T_BOTTOM },
|
||||
'┴': { [Style.NORMAL]: Shapes.T_TOP },
|
||||
'┻': { [Style.BOLD]: Shapes.T_TOP },
|
||||
'┼': { [Style.NORMAL]: Shapes.CROSS },
|
||||
'╋': { [Style.BOLD]: Shapes.CROSS },
|
||||
'╴': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT },
|
||||
'╸': { [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'╵': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP },
|
||||
'╹': { [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'╶': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'╺': { [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'╷': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'╻': { [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
|
||||
// Double border
|
||||
'═': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
||||
'║': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
||||
'╒': { [Style.NORMAL]: (xp, yp) => `M.5,1 L.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
|
||||
'╓': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},1 L${.5 - xp},.5 L1,.5 M${.5 + xp},.5 L${.5 + xp},1` },
|
||||
'╔': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
|
||||
'╕': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L.5,${.5 - yp} L.5,1 M0,${.5 + yp} L.5,${.5 + yp}` },
|
||||
'╖': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},1 L${.5 + xp},.5 L0,.5 M${.5 - xp},.5 L${.5 - xp},1` },
|
||||
'╗': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},1` },
|
||||
'╘': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 + yp} L1,${.5 + yp} M.5,${.5 - yp} L1,${.5 - yp}` },
|
||||
'╙': { [Style.NORMAL]: (xp, yp) => `M1,.5 L${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
|
||||
'╚': { [Style.NORMAL]: (xp, yp) => `M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0 M1,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},0` },
|
||||
'╛': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L.5,${.5 + yp} L.5,0 M0,${.5 - yp} L.5,${.5 - yp}` },
|
||||
'╜': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 + xp},.5 L${.5 + xp},0 M${.5 - xp},.5 L${.5 - xp},0` },
|
||||
'╝': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M0,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},0` },
|
||||
'╞': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M.5,${.5 - yp} L1,${.5 - yp} M.5,${.5 + yp} L1,${.5 + yp}` },
|
||||
'╟': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1 M${.5 + xp},.5 L1,.5` },
|
||||
'╠': { [Style.NORMAL]: (xp, yp) => `M${.5 - xp},0 L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
||||
'╡': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L.5,${.5 - yp} M0,${.5 + yp} L.5,${.5 + yp}` },
|
||||
'╢': { [Style.NORMAL]: (xp, yp) => `M0,.5 L${.5 - xp},.5 M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
||||
'╣': { [Style.NORMAL]: (xp, yp) => `M${.5 + xp},0 L${.5 + xp},1 M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0` },
|
||||
'╤': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp} M.5,${.5 + yp} L.5,1` },
|
||||
'╥': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},1 M${.5 + xp},.5 L${.5 + xp},1` },
|
||||
'╦': { [Style.NORMAL]: (xp, yp) => `M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1` },
|
||||
'╧': { [Style.NORMAL]: (xp, yp) => `M.5,0 L.5,${.5 - yp} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
||||
'╨': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},.5 L${.5 - xp},0 M${.5 + xp},.5 L${.5 + xp},0` },
|
||||
'╩': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L1,${.5 + yp} M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
||||
'╪': { [Style.NORMAL]: (xp, yp) => `${Shapes.TOP_TO_BOTTOM} M0,${.5 - yp} L1,${.5 - yp} M0,${.5 + yp} L1,${.5 + yp}` },
|
||||
'╫': { [Style.NORMAL]: (xp, yp) => `${Shapes.LEFT_TO_RIGHT} M${.5 - xp},0 L${.5 - xp},1 M${.5 + xp},0 L${.5 + xp},1` },
|
||||
'╬': { [Style.NORMAL]: (xp, yp) => `M0,${.5 + yp} L${.5 - xp},${.5 + yp} L${.5 - xp},1 M1,${.5 + yp} L${.5 + xp},${.5 + yp} L${.5 + xp},1 M0,${.5 - yp} L${.5 - xp},${.5 - yp} L${.5 - xp},0 M1,${.5 - yp} L${.5 + xp},${.5 - yp} L${.5 + xp},0` },
|
||||
|
||||
// Diagonal
|
||||
'╱': { [Style.NORMAL]: 'M1,0 L0,1' },
|
||||
'╲': { [Style.NORMAL]: 'M0,0 L1,1' },
|
||||
'╳': { [Style.NORMAL]: 'M1,0 L0,1 M0,0 L1,1' },
|
||||
|
||||
// Mixed weight
|
||||
'╼': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'╽': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'╾': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'╿': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┍': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┎': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'┑': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┒': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'┕': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┖': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┙': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┚': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┝': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┞': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┟': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'┠': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
|
||||
'┡': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
|
||||
'┢': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
|
||||
'┥': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┦': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┧': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'┨': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
|
||||
'┩': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT },
|
||||
'┪': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
|
||||
'┭': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┮': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┯': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
|
||||
'┰': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'┱': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
|
||||
'┲': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
|
||||
'┵': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┶': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┷': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
|
||||
'┸': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'┹': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_LEFT },
|
||||
'┺': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
|
||||
'┽': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_LEFT },
|
||||
'┾': { [Style.NORMAL]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}`, [Style.BOLD]: Shapes.MIDDLE_TO_RIGHT },
|
||||
'┿': { [Style.NORMAL]: Shapes.TOP_TO_BOTTOM, [Style.BOLD]: Shapes.LEFT_TO_RIGHT },
|
||||
'╀': { [Style.NORMAL]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}`, [Style.BOLD]: Shapes.MIDDLE_TO_TOP },
|
||||
'╁': { [Style.NORMAL]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}`, [Style.BOLD]: Shapes.MIDDLE_TO_BOTTOM },
|
||||
'╂': { [Style.NORMAL]: Shapes.LEFT_TO_RIGHT, [Style.BOLD]: Shapes.TOP_TO_BOTTOM },
|
||||
'╃': { [Style.NORMAL]: Shapes.RIGHT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_LEFT },
|
||||
'╄': { [Style.NORMAL]: Shapes.LEFT_TO_BOTTOM, [Style.BOLD]: Shapes.TOP_TO_RIGHT },
|
||||
'╅': { [Style.NORMAL]: Shapes.TOP_TO_RIGHT, [Style.BOLD]: Shapes.LEFT_TO_BOTTOM },
|
||||
'╆': { [Style.NORMAL]: Shapes.TOP_TO_LEFT, [Style.BOLD]: Shapes.RIGHT_TO_BOTTOM },
|
||||
'╇': { [Style.NORMAL]: Shapes.MIDDLE_TO_BOTTOM, [Style.BOLD]: `${Shapes.MIDDLE_TO_TOP} ${Shapes.LEFT_TO_RIGHT}` },
|
||||
'╈': { [Style.NORMAL]: Shapes.MIDDLE_TO_TOP, [Style.BOLD]: `${Shapes.LEFT_TO_RIGHT} ${Shapes.MIDDLE_TO_BOTTOM}` },
|
||||
'╉': { [Style.NORMAL]: Shapes.MIDDLE_TO_RIGHT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_LEFT}` },
|
||||
'╊': { [Style.NORMAL]: Shapes.MIDDLE_TO_LEFT, [Style.BOLD]: `${Shapes.TOP_TO_BOTTOM} ${Shapes.MIDDLE_TO_RIGHT}` },
|
||||
|
||||
// Dashed
|
||||
'╌': { [Style.NORMAL]: Shapes.TWO_DASHES_HORIZONTAL },
|
||||
'╍': { [Style.BOLD]: Shapes.TWO_DASHES_HORIZONTAL },
|
||||
'┄': { [Style.NORMAL]: Shapes.THREE_DASHES_HORIZONTAL },
|
||||
'┅': { [Style.BOLD]: Shapes.THREE_DASHES_HORIZONTAL },
|
||||
'┈': { [Style.NORMAL]: Shapes.FOUR_DASHES_HORIZONTAL },
|
||||
'┉': { [Style.BOLD]: Shapes.FOUR_DASHES_HORIZONTAL },
|
||||
'╎': { [Style.NORMAL]: Shapes.TWO_DASHES_VERTICAL },
|
||||
'╏': { [Style.BOLD]: Shapes.TWO_DASHES_VERTICAL },
|
||||
'┆': { [Style.NORMAL]: Shapes.THREE_DASHES_VERTICAL },
|
||||
'┇': { [Style.BOLD]: Shapes.THREE_DASHES_VERTICAL },
|
||||
'┊': { [Style.NORMAL]: Shapes.FOUR_DASHES_VERTICAL },
|
||||
'┋': { [Style.BOLD]: Shapes.FOUR_DASHES_VERTICAL },
|
||||
|
||||
// Curved
|
||||
'╭': { [Style.NORMAL]: 'C.5,1,.5,.5,1,.5' },
|
||||
'╮': { [Style.NORMAL]: 'C.5,1,.5,.5,0,.5' },
|
||||
'╯': { [Style.NORMAL]: 'C.5,0,.5,.5,0,.5' },
|
||||
'╰': { [Style.NORMAL]: 'C.5,0,.5,.5,1,.5' }
|
||||
};
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
* successfully drawn.
|
||||
*/
|
||||
export function tryDrawCustomChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
c: string,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): boolean {
|
||||
const blockElementDefinition = blockElementDefinitions[c];
|
||||
if (blockElementDefinition) {
|
||||
drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
const patternDefinition = patternCharacterDefinitions[c];
|
||||
if (patternDefinition) {
|
||||
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
const boxDrawingDefinition = boxDrawingDefinitions[c];
|
||||
if (boxDrawingDefinition) {
|
||||
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawBlockElementChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: IBlockVector[],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): void {
|
||||
for (let i = 0; i < charDefinition.length; i++) {
|
||||
const box = charDefinition[i];
|
||||
const xEighth = scaledCellWidth / 8;
|
||||
const yEighth = scaledCellHeight / 8;
|
||||
ctx.fillRect(
|
||||
xOffset + box.x * xEighth,
|
||||
yOffset + box.y * yEighth,
|
||||
box.w * xEighth,
|
||||
box.h * yEighth
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const cachedPatterns: Map<PatternDefinition, Map</* fillStyle */string, CanvasPattern>> = new Map();
|
||||
|
||||
function drawPatternChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: number[][],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): void {
|
||||
let patternSet = cachedPatterns.get(charDefinition);
|
||||
if (!patternSet) {
|
||||
patternSet = new Map();
|
||||
cachedPatterns.set(charDefinition, patternSet);
|
||||
}
|
||||
const fillStyle = ctx.fillStyle;
|
||||
if (typeof fillStyle !== 'string') {
|
||||
throw new Error(`Unexpected fillStyle type "${fillStyle}"`);
|
||||
}
|
||||
let pattern = patternSet.get(fillStyle);
|
||||
if (!pattern) {
|
||||
const width = charDefinition[0].length;
|
||||
const height = charDefinition.length;
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
tmpCanvas.width = width;
|
||||
tmpCanvas.height = height;
|
||||
const tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d'));
|
||||
const imageData = new ImageData(width, height);
|
||||
|
||||
// Extract rgba from fillStyle
|
||||
let r: number;
|
||||
let g: number;
|
||||
let b: number;
|
||||
let a: number;
|
||||
if (fillStyle.startsWith('#')) {
|
||||
r = parseInt(fillStyle.substr(1, 2), 16);
|
||||
g = parseInt(fillStyle.substr(3, 2), 16);
|
||||
b = parseInt(fillStyle.substr(5, 2), 16);
|
||||
a = fillStyle.length > 7 && parseInt(fillStyle.substr(7, 2), 16) || 1;
|
||||
} else if (fillStyle.startsWith('rgba')) {
|
||||
([r, g, b, a] = fillStyle.substring(5, fillStyle.length - 1).split(',').map(e => parseFloat(e)));
|
||||
} else {
|
||||
throw new Error(`Unexpected fillStyle color format "${fillStyle}" when drawing pattern glyph`);
|
||||
}
|
||||
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
imageData.data[(y * width + x) * 4 ] = r;
|
||||
imageData.data[(y * width + x) * 4 + 1] = g;
|
||||
imageData.data[(y * width + x) * 4 + 2] = b;
|
||||
imageData.data[(y * width + x) * 4 + 3] = charDefinition[y][x] * (a * 255);
|
||||
}
|
||||
}
|
||||
tmpCtx.putImageData(imageData, 0, 0);
|
||||
pattern = throwIfFalsy(ctx.createPattern(tmpCanvas, null));
|
||||
patternSet.set(fillStyle, pattern);
|
||||
}
|
||||
ctx.fillStyle = pattern;
|
||||
ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
|
||||
* canvas draw calls.
|
||||
*
|
||||
* Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐
|
||||
* ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘
|
||||
* ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤
|
||||
* └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘
|
||||
*
|
||||
* Other:
|
||||
* ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈
|
||||
* │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉
|
||||
* ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋
|
||||
*
|
||||
* All box drawing characters:
|
||||
* ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
||||
* ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
||||
* ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
||||
* ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
||||
* ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
||||
* ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
||||
* ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
||||
* ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Box drawing alignment tests: █
|
||||
* ▉
|
||||
* ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
|
||||
* ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
|
||||
* ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
|
||||
* ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
|
||||
* ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
|
||||
* ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
|
||||
* ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
|
||||
*
|
||||
* Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
|
||||
*/
|
||||
function drawBoxDrawingChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
): void {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
for (const [fontWeight, instructions] of Object.entries(charDefinition)) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = window.devicePixelRatio * Number.parseInt(fontWeight);
|
||||
let actualInstructions: string;
|
||||
if (typeof instructions === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / scaledCellHeight * scaledCellWidth;
|
||||
actualInstructions = instructions(xp, yp);
|
||||
} else {
|
||||
actualInstructions = instructions;
|
||||
}
|
||||
for (const instruction of actualInstructions.split(' ')) {
|
||||
const type = instruction[0];
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
|
||||
const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||
'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]),
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1])
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number): number[] {
|
||||
const result = args.map(e => parseFloat(e) || parseInt(e));
|
||||
|
||||
if (result.length < 2) {
|
||||
throw new Error('Too few arguments for instruction');
|
||||
}
|
||||
|
||||
for (let x = 0; x < result.length; x += 2) {
|
||||
// Translate from 0-1 to 0-cellWidth
|
||||
result[x] *= cellWidth;
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[x] !== 0) {
|
||||
result[x] = clamp(Math.round(result[x] + 0.5) - 0.5, cellWidth, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellWidth)
|
||||
result[x] += xOffset;
|
||||
}
|
||||
|
||||
for (let y = 1; y < result.length; y += 2) {
|
||||
// Translate from 0-1 to 0-cellHeight
|
||||
result[y] *= cellHeight;
|
||||
// Ensure coordinate doesn't escape cell bounds and round to the nearest 0.5 to ensure a crisp
|
||||
// line at 100% devicePixelRatio
|
||||
if (result[y] !== 0) {
|
||||
result[y] = clamp(Math.round(result[y] + 0.5) - 0.5, cellHeight, 0);
|
||||
}
|
||||
// Apply the cell's offset (ie. x*cellHeight)
|
||||
result[y] += yOffset;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -20,8 +20,8 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
rendererId: number,
|
||||
linkifier: ILinkifier,
|
||||
linkifier2: ILinkifier2,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
|
||||
@@ -6,13 +6,12 @@
|
||||
import { TextRenderLayer } from 'browser/renderer/TextRenderLayer';
|
||||
import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer';
|
||||
import { IRenderLayer, IRenderer, IRenderDimensions, CharacterJoinerHandler, ICharacterJoinerRegistry, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer';
|
||||
import { CharacterJoinerRegistry } from 'browser/renderer/CharacterJoinerRegistry';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { IBufferService, IOptionsService, ICoreService, IInstantiationService } from 'common/services/Services';
|
||||
import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
|
||||
@@ -23,7 +22,6 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
@@ -35,20 +33,18 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
private readonly _screenElement: HTMLElement,
|
||||
linkifier: ILinkifier,
|
||||
linkifier2: ILinkifier2,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICoreService coreService: ICoreService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
const allowTransparency = this._optionsService.options.allowTransparency;
|
||||
this._characterJoinerRegistry = new CharacterJoinerRegistry(this._bufferService);
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._screenElement, 0, this._colors, this._characterJoinerRegistry, allowTransparency, this._id, this._bufferService, _optionsService),
|
||||
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, _optionsService),
|
||||
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier, linkifier2, this._bufferService, _optionsService),
|
||||
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, _optionsService, coreService, coreBrowserService)
|
||||
instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id),
|
||||
instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id),
|
||||
instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2),
|
||||
instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
@@ -153,6 +149,12 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
for (const layer of this._renderLayers) {
|
||||
layer.clearTextureAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
@@ -210,12 +212,4 @@ export class Renderer extends Disposable implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService);
|
||||
this._clearState();
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerRegistry, IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { JoinedCellData } from 'browser/renderer/CharacterJoinerRegistry';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService } from 'common/services/Services';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -26,22 +27,20 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterWidth: number = 0;
|
||||
private _characterFont: string = '';
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
characterJoinerRegistry: ICharacterJoinerRegistry,
|
||||
alpha: boolean,
|
||||
rendererId: number,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -67,7 +66,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _forEachCell(
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
cell: ICellData,
|
||||
x: number,
|
||||
@@ -77,7 +75,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const line = this._bufferService.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (let x = 0; x < this._bufferService.cols; x++) {
|
||||
line!.loadCell(x, this._workCell);
|
||||
let cell = this._workCell;
|
||||
@@ -101,7 +99,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// 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(
|
||||
this._workCell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
@@ -160,7 +157,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(firstRow, lastRow, null, (cell, x, y) => {
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
@@ -213,12 +210,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(firstRow, lastRow, this._characterJoinerRegistry, (cell, x, y) => {
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
return;
|
||||
}
|
||||
this._drawChars(cell, x, y);
|
||||
if (cell.isUnderline()) {
|
||||
if (cell.isUnderline() || cell.isStrikethrough()) {
|
||||
this._ctx.save();
|
||||
|
||||
if (cell.isInverse()) {
|
||||
@@ -247,7 +244,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
if (cell.isStrikethrough()) {
|
||||
this._fillMiddleLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
this._ctx.restore();
|
||||
}
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user