Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a11ba0a89c | ||
|
|
2134e050cb | ||
|
|
e2ed603ab8 | ||
|
|
ff3781a431 | ||
|
|
55cd97ec68 | ||
|
|
2f0a4fc5c0 | ||
|
|
6dd61f96c0 | ||
|
|
fdae61380c | ||
|
|
09acb2b5f9 | ||
|
|
19c9fef8e2 | ||
|
|
c335682559 | ||
|
|
96806ee384 | ||
|
|
096655a3eb | ||
|
|
a9788c364d | ||
|
|
f593d14823 | ||
|
|
6801793547 | ||
|
|
f52d631fa4 | ||
|
|
ca70d7b6b4 | ||
|
|
fe1d2f6af1 | ||
|
|
64b37ef518 | ||
|
|
654a0a671d | ||
|
|
dc1b1b4537 | ||
|
|
579e43c4cc | ||
|
|
f8d8288e0e | ||
|
|
ea7f89842a | ||
|
|
91a79e9dee | ||
|
|
a385ab0802 | ||
|
|
9175a220be | ||
|
|
0131fc9ebf | ||
|
|
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 | ||
|
|
cdd238d75e | ||
|
|
48a93c379f | ||
|
|
cc005a274d | ||
|
|
26bf1ab840 | ||
|
|
b4635eddf3 | ||
|
|
6b57921567 | ||
|
|
d500b0f204 | ||
|
|
9985ae2c38 | ||
|
|
bce3269e44 | ||
|
|
187f072f43 | ||
|
|
3ab2376517 | ||
|
|
26502114d4 | ||
|
|
5db479c25d | ||
|
|
d0b555687f | ||
|
|
041e259c11 | ||
|
|
d59f4d2f6f | ||
|
|
898a18db21 | ||
|
|
a73fe62b7a | ||
|
|
350f6cfea1 | ||
|
|
2936f80013 | ||
|
|
dec9f6d254 | ||
|
|
bd6676d3b6 | ||
|
|
d9309d33af | ||
|
|
bb9802e754 | ||
|
|
eec804dd7c | ||
|
|
ff6d3a0768 | ||
|
|
94d37bbe4c | ||
|
|
36136a4c29 | ||
|
|
8b273e6939 | ||
|
|
2340fa1c91 | ||
|
|
70051da2ab | ||
|
|
5659bf3283 | ||
|
|
8de8ac10d4 | ||
|
|
46c93f1b63 | ||
|
|
e3a9d0d14f | ||
|
|
0a4b403f6a | ||
|
|
b94b4f5353 | ||
|
|
d84576e160 | ||
|
|
a0903fbf9b | ||
|
|
4b29b8a9c6 | ||
|
|
c994c67dd9 | ||
|
|
0b09f0bafd | ||
|
|
54bb0ee189 | ||
|
|
68b7f88523 | ||
|
|
a9eb1de4b4 | ||
|
|
28bcd88fb0 | ||
|
|
bee4b0ff06 | ||
|
|
559b3c1392 | ||
|
|
d3c9f70bec | ||
|
|
4ba7aef441 | ||
|
|
2f07ed9976 | ||
|
|
ff74caf50d | ||
|
|
3d941f2fdf | ||
|
|
93aab95f7b | ||
|
|
079702282c | ||
|
|
db8e449a8a | ||
|
|
a2480bae51 | ||
|
|
cd501f9dcc | ||
|
|
354f3e03a2 | ||
|
|
b07ca8bb24 | ||
|
|
22d777c9f9 | ||
|
|
adfeffec76 | ||
|
|
4a1bdad759 | ||
|
|
00ef60ebaf | ||
|
|
0ad9f70645 | ||
|
|
4da5d557a5 | ||
|
|
a1dc85ee57 | ||
|
|
554b4a8224 | ||
|
|
3f659a15da | ||
|
|
dd5b295219 | ||
|
|
9d0200252b | ||
|
|
d67faff92a | ||
|
|
c50c134e25 | ||
|
|
bc1048062d | ||
|
|
74ea558bfc | ||
|
|
c6f93701f5 | ||
|
|
8a4772ca39 | ||
|
|
66c80d8b78 | ||
|
|
0cb38cfdcb | ||
|
|
b0605e4cd1 | ||
|
|
82a9ee62d7 | ||
|
|
8b2b0f6abc | ||
|
|
c669c3fba7 | ||
|
|
8860726c68 | ||
|
|
f64724f54f | ||
|
|
fd222b58b5 | ||
|
|
83ba481e3d | ||
|
|
7d5bbc341f | ||
|
|
519e44d30e | ||
|
|
3bb2c158fd | ||
|
|
5ada5f0c0c | ||
|
|
ea1fcc6a5f | ||
|
|
19c14b076b | ||
|
|
676aabc3ed | ||
|
|
5c33e4008d | ||
|
|
772e446192 | ||
|
|
66be7797cf | ||
|
|
3261c53c08 | ||
|
|
5bab94730f | ||
|
|
801aeb8462 | ||
|
|
cbe5abcf27 | ||
|
|
72dbd43be6 | ||
|
|
7145130835 | ||
|
|
b34289cc44 | ||
|
|
fe2b1c0d43 | ||
|
|
4de6f1c255 | ||
|
|
8baff9bade | ||
|
|
f8f7e7e4c1 | ||
|
|
7523505af1 | ||
|
|
d94b119e9d | ||
|
|
3cab8511e4 | ||
|
|
7d3f5375a8 | ||
|
|
b2c4ed638b | ||
|
|
2804bfa8b7 | ||
|
|
64ac67026a | ||
|
|
5a74ce2e59 | ||
|
|
f4a56389cd | ||
|
|
52b64beefc | ||
|
|
ec1234e8d8 | ||
|
|
b7695d2fd6 | ||
|
|
4c6dc45d20 | ||
|
|
5cb048094e | ||
|
|
e3cccd619b | ||
|
|
88f0b91056 | ||
|
|
78ddb9feba | ||
|
|
f68ff2a455 | ||
|
|
04e2bdd6ec | ||
|
|
84fa6be370 | ||
|
|
af2f744186 | ||
|
|
5f4b719444 | ||
|
|
9616851e52 | ||
|
|
4645fda126 | ||
|
|
057a8f6f1b | ||
|
|
e6917d1933 | ||
|
|
6535995753 | ||
|
|
77ed5ba7ed | ||
|
|
bc86b7804c | ||
|
|
d5f4ae53c9 | ||
|
|
b898e016f3 | ||
|
|
712f0ae2a7 | ||
|
|
c553107d23 | ||
|
|
b127436cc4 | ||
|
|
b61ac11a76 | ||
|
|
b22d7f1140 | ||
|
|
925aaf18a1 | ||
|
|
797ed47791 | ||
|
|
4128a6e2f1 | ||
|
|
69f34be4b8 | ||
|
|
1c3da7e2e6 | ||
|
|
5c7644adc0 | ||
|
|
f974010aa3 | ||
|
|
d4d18f741f | ||
|
|
d97b54b9be | ||
|
|
a7b94fbe1a | ||
|
|
2ab9de91fe | ||
|
|
fd6bf6d667 | ||
|
|
38e81f053f | ||
|
|
c6f1b456f5 | ||
|
|
8484978ef0 | ||
|
|
c4919b9a6b | ||
|
|
02e4db4c82 | ||
|
|
5200766617 | ||
|
|
8d9badb588 | ||
|
|
fc534a1067 | ||
|
|
f68ef19868 | ||
|
|
0a2e40502f | ||
|
|
e843a2f61e | ||
|
|
d95a633f16 | ||
|
|
6f41aad2ef | ||
|
|
e76cbd9c3f | ||
|
|
0c8b299996 | ||
|
|
86420d2535 | ||
|
|
9e4dcdaf91 | ||
|
|
45e7bf274a | ||
|
|
62cf413d54 | ||
|
|
3e0a270891 | ||
|
|
22d79eef26 | ||
|
|
31d806afce | ||
|
|
1e9ca83120 | ||
|
|
3d9a33bb69 | ||
|
|
ea989ed691 | ||
|
|
ee2c5969fa | ||
|
|
8e151ee6ef | ||
|
|
dc904b27a2 | ||
|
|
4de539a1ff | ||
|
|
913de3158e | ||
|
|
049ca653ed | ||
|
|
246d137c2c | ||
|
|
99b1a7a49b | ||
|
|
9fa068f574 | ||
|
|
eb5f964bcb | ||
|
|
91337d7e57 | ||
|
|
cb555d5297 | ||
|
|
50dd3cb841 | ||
|
|
b7750cbf91 | ||
|
|
dde47269de | ||
|
|
bc3f2a12c0 | ||
|
|
bdb112b23c | ||
|
|
214ccd14ac | ||
|
|
e5f3820285 | ||
|
|
f75b4cfa93 | ||
|
|
f5d651dda1 | ||
|
|
a90254e778 | ||
|
|
a6bcfcfba4 | ||
|
|
78ae1d09f7 | ||
|
|
1132da8563 | ||
|
|
199b9ceb6d | ||
|
|
8e41d22f8d | ||
|
|
57bd61697c | ||
|
|
3e8b8dc6ef | ||
|
|
b4bc830cee | ||
|
|
2e43f615f6 | ||
|
|
5f2d1d2b4e | ||
|
|
4ee305fa21 | ||
|
|
4b3e16dbd0 | ||
|
|
a7c836e42c | ||
|
|
886c057d0f | ||
|
|
038704786d | ||
|
|
c61076830f | ||
|
|
02fccfbdc7 | ||
|
|
b250cedf81 | ||
|
|
7243e330fc | ||
|
|
17a7fb2a6e | ||
|
|
d3d8ec3b20 | ||
|
|
b6074df4bf | ||
|
|
f3cc9469a7 | ||
|
|
a9d2b8a55c | ||
|
|
73e235d9f8 | ||
|
|
17ec85e39e | ||
|
|
723c64e481 | ||
|
|
faa60be6fb | ||
|
|
e95f9815d8 | ||
|
|
9b62d8a01a | ||
|
|
1c206c2c27 | ||
|
|
782797ca11 | ||
|
|
b34ee5157e | ||
|
|
ae37c43cd4 | ||
|
|
272b1d2194 | ||
|
|
260e2dab16 | ||
|
|
7c78390e2b | ||
|
|
f5a4eed27c | ||
|
|
b0768f18ca | ||
|
|
4d1e532069 | ||
|
|
5beb7f52c2 | ||
|
|
eaaf9b7af1 | ||
|
|
3c9687decf | ||
|
|
a5e269a3f9 | ||
|
|
30d84b1202 | ||
|
|
0aa1079cd0 | ||
|
|
1091255d2c | ||
|
|
56e63d762b | ||
|
|
f3c8b8f395 | ||
|
|
47be9a2df4 | ||
|
|
aee1629c25 | ||
|
|
8587eea54c | ||
|
|
d7ed109f9d | ||
|
|
84dfe8fccc | ||
|
|
6904dc5009 | ||
|
|
b89227d365 | ||
|
|
6c01630090 | ||
|
|
10282e4cb1 | ||
|
|
8518b354a7 | ||
|
|
00a8b80ef7 | ||
|
|
1a0a56c1f2 | ||
|
|
055267ee9c | ||
|
|
6e9040e6ba | ||
|
|
a32776e2af | ||
|
|
1d6d6eb716 | ||
|
|
881a4a0dbb | ||
|
|
003cf96910 | ||
|
|
3cb87af315 | ||
|
|
edee1a1067 | ||
|
|
e32d8774eb | ||
|
|
49d5608fa6 | ||
|
|
56bfdb64d4 | ||
|
|
9513c231c9 | ||
|
|
dd92161da5 | ||
|
|
55571a1ae8 | ||
|
|
c112990d8f | ||
|
|
23dba6aebd | ||
|
|
f669b8ed1d | ||
|
|
9a22cbe44b | ||
|
|
883ddba934 | ||
|
|
8f7295b8a2 | ||
|
|
0ba97f6941 | ||
|
|
d979c077a0 | ||
|
|
4e18a78c02 | ||
|
|
1d6e8abcca | ||
|
|
82a65be446 | ||
|
|
0778a823d1 | ||
|
|
eca36458b3 | ||
|
|
3504e2ebd5 | ||
|
|
6c22e3df6b | ||
|
|
33bf765b80 | ||
|
|
d960793f0c | ||
|
|
4975f389ff | ||
|
|
4649ad7e7b | ||
|
|
a425c325b7 | ||
|
|
c77025b18c | ||
|
|
c6abc85810 | ||
|
|
710223ab27 | ||
|
|
ca9df42a73 | ||
|
|
9f033cb1d2 | ||
|
|
9613f36280 | ||
|
|
41673f2c20 | ||
|
|
6c98f82ffe | ||
|
|
a90ef52bda | ||
|
|
86884d2d88 | ||
|
|
47c486e1be | ||
|
|
e07b9684bb | ||
|
|
10fb48869e | ||
|
|
9f3746a3a1 | ||
|
|
50fc84f7d7 | ||
|
|
3e0f0331b3 | ||
|
|
b918f0d34a | ||
|
|
f0e3159ece | ||
|
|
ecfc7336c5 | ||
|
|
aec8c68bf6 | ||
|
|
0fb63561fb | ||
|
|
106ea62b4c | ||
|
|
e73bf11849 | ||
|
|
8d9eda5d0a | ||
|
|
cf0b7ce2d0 | ||
|
|
60419a7cc6 | ||
|
|
1b12b2d15d | ||
|
|
b5844990c4 | ||
|
|
2652768310 | ||
|
|
1f8a6bece4 | ||
|
|
8817ddf020 | ||
|
|
5d61956aff | ||
|
|
83ece031a7 | ||
|
|
566fad4108 | ||
|
|
a4aa111eaa | ||
|
|
f6ed184d49 | ||
|
|
c1820501a8 | ||
|
|
c7f50c5c28 | ||
|
|
371c96e1da | ||
|
|
11bd8b7eb1 | ||
|
|
cb418eb8d1 | ||
|
|
f811fb937e | ||
|
|
2a6ccb8009 | ||
|
|
bf49f99810 | ||
|
|
e027a5f97b | ||
|
|
d1ecb571d8 | ||
|
|
1e3937830f | ||
|
|
38b2fcb460 | ||
|
|
c7ed42226f | ||
|
|
d0dfad4c92 | ||
|
|
c66123b91f | ||
|
|
8d912e891e | ||
|
|
23fd6b8e60 | ||
|
|
0320e6e22a | ||
|
|
9e70d8fc84 | ||
|
|
27e28eccac | ||
|
|
0cb0313fc9 | ||
|
|
94a34c3703 | ||
|
|
c04c836f12 | ||
|
|
c5a9d2c52d | ||
|
|
c0050672ab | ||
|
|
705f94518f | ||
|
|
a096217526 | ||
|
|
795768667e | ||
|
|
5e9a8c6321 | ||
|
|
03df0565bd | ||
|
|
3ef6856b36 | ||
|
|
48e9f33263 | ||
|
|
9559dd47da | ||
|
|
ad7f8c303c | ||
|
|
edb7a0edda | ||
|
|
aedcbba211 | ||
|
|
ab63b51681 | ||
|
|
63f5d0c266 | ||
|
|
349ebd3846 | ||
|
|
ce496bc389 | ||
|
|
be1af82ee0 | ||
|
|
11bfb03add | ||
|
|
b89d4b102e | ||
|
|
0abd121653 | ||
|
|
eeb095b095 | ||
|
|
266a289520 | ||
|
|
3c1d6b575a | ||
|
|
bd08862074 | ||
|
|
70e0c0385a | ||
|
|
454cf1fb26 | ||
|
|
6d1029b3c7 | ||
|
|
f13f8fb38a | ||
|
|
2447064604 | ||
|
|
85a2589c0c | ||
|
|
2c821155ae | ||
|
|
c040be1ad2 | ||
|
|
2b91bb83fa | ||
|
|
5738f4545f | ||
|
|
01432a31d6 | ||
|
|
798a7fd52e | ||
|
|
cee121f414 | ||
|
|
a4fe871865 | ||
|
|
d85c80df95 | ||
|
|
95d01dec52 | ||
|
|
1506097faa | ||
|
|
dc541c2664 | ||
|
|
694da35f99 | ||
|
|
3c7697420c | ||
|
|
4a407dbe76 | ||
|
|
2479660028 | ||
|
|
612b922eeb | ||
|
|
ea2336b68e | ||
|
|
0c953a88c3 | ||
|
|
a551c69e9e | ||
|
|
7e7be15ab8 | ||
|
|
cb0242782a | ||
|
|
04655b1db5 | ||
|
|
7ce47c474a | ||
|
|
b6259d3e7e | ||
|
|
3e6bcc09b3 | ||
|
|
fbc34b649c | ||
|
|
4a0cb00450 | ||
|
|
0cf32d9b4d | ||
|
|
f6bf7dcd92 | ||
|
|
4f59d1561a | ||
|
|
86af9aa59d | ||
|
|
1cd4f79347 | ||
|
|
f928c7e156 | ||
|
|
0e6611b528 | ||
|
|
afc39106c3 | ||
|
|
06c8de381b | ||
|
|
ac0e1aeb19 | ||
|
|
1171a20262 | ||
|
|
8fa729a702 | ||
|
|
53eb54e656 | ||
|
|
3b9047be0f | ||
|
|
2562311606 | ||
|
|
98d0760898 | ||
|
|
4c943ddb0d | ||
|
|
61432ab65d | ||
|
|
0ccdc8d1ca | ||
|
|
8a3366b1eb | ||
|
|
c669cfbb23 | ||
|
|
45749f6017 | ||
|
|
bc6bb417ac | ||
|
|
5342ba6b10 | ||
|
|
a1dc0d1936 | ||
|
|
15f1759580 | ||
|
|
fb25be3ecd | ||
|
|
fbb8c6273b | ||
|
|
289014b32e | ||
|
|
9b91e3d5e2 | ||
|
|
d57e099e6e | ||
|
|
c5d30a9c63 | ||
|
|
8ee08a3be7 | ||
|
|
5af1554bbf | ||
|
|
e226a111f9 | ||
|
|
d5bc65ec0e | ||
|
|
106d74caa2 | ||
|
|
825784e0ec | ||
|
|
80c6c7f399 | ||
|
|
156563e832 | ||
|
|
b92260870a | ||
|
|
04f2b6e9ff | ||
|
|
a9862a1a6f | ||
|
|
2e2acd77f3 | ||
|
|
75e8999a88 | ||
|
|
7bf62a3078 | ||
|
|
0030ed214d | ||
|
|
f63f1f7b95 | ||
|
|
6daa3f4499 | ||
|
|
4e6151d011 | ||
|
|
18330b47ed | ||
|
|
864f1a9be9 | ||
|
|
1b6e24d94a | ||
|
|
617e702a94 | ||
|
|
6c0de23915 | ||
|
|
869d791f44 | ||
|
|
e5499711c3 | ||
|
|
3fbd215854 | ||
|
|
b7132e7fa7 | ||
|
|
7f201e3e6b | ||
|
|
cebbac8265 | ||
|
|
227a1f5aa3 | ||
|
|
07bdc10245 | ||
|
|
1b9b6f0cbf | ||
|
|
1f6ebb3dac | ||
|
|
3abb66500d | ||
|
|
b6b2e67180 | ||
|
|
db9d950fb4 | ||
|
|
bdd8b8d32f | ||
|
|
d8bc7ceaff | ||
|
|
6a0f7b0713 | ||
|
|
3ac8a89f9b | ||
|
|
fcec9c793c | ||
|
|
2140930793 | ||
|
|
2c16a7e0d7 | ||
|
|
325559e5d1 | ||
|
|
3ec926750a | ||
|
|
6d8e9c43c9 | ||
|
|
69f3f302d6 | ||
|
|
1282900140 | ||
|
|
768710b2d7 | ||
|
|
8750ad4ec2 | ||
|
|
409d995080 | ||
|
|
a151b72fc5 | ||
|
|
0626b7ec22 | ||
|
|
91205710ef | ||
|
|
5a4f352bb0 | ||
|
|
f62a14e976 | ||
|
|
5f3fb410f7 | ||
|
|
2d8dfde660 | ||
|
|
7fb885ce7d | ||
|
|
4aa3097cd1 | ||
|
|
0b87d9a39c | ||
|
|
92fdf0ce58 | ||
|
|
aa59f34aab | ||
|
|
05c149783f | ||
|
|
c3bc436e49 | ||
|
|
64328ee39c | ||
|
|
61667c1c23 | ||
|
|
3a9cdbab0d | ||
|
|
27af770068 | ||
|
|
71e45cdd25 | ||
|
|
f9adf8ad83 | ||
|
|
f886698dc3 | ||
|
|
7ab0a42b24 | ||
|
|
99e2a3db1c | ||
|
|
9ca6733f68 | ||
|
|
6d5ae1416a | ||
|
|
0707398a5b | ||
|
|
75e4e2089e | ||
|
|
484883f442 | ||
|
|
d488b49fc4 | ||
|
|
1168c6c52f | ||
|
|
f4e87cdbb3 | ||
|
|
24c64bfc7f | ||
|
|
ed5aeb7472 | ||
|
|
eb3f933290 | ||
|
|
832c43a509 | ||
|
|
31d3ae451c | ||
|
|
e413133602 | ||
|
|
06b8d36cf0 | ||
|
|
b75e42a56c | ||
|
|
66b2c01549 | ||
|
|
065eb13a9d | ||
|
|
15bb9aa137 | ||
|
|
07862c2e52 | ||
|
|
a8b6252c3c | ||
|
|
bbbf128b7c | ||
|
|
62b8398719 | ||
|
|
3ea9a7511e | ||
|
|
076d51fc35 | ||
|
|
210fa7f655 | ||
|
|
fbdb01d43c | ||
|
|
36e241cd8d | ||
|
|
c5b0068c9b | ||
|
|
ed7bab7f02 | ||
|
|
001e16e081 | ||
|
|
41670dc181 | ||
|
|
6c6900ede8 | ||
|
|
b450470142 | ||
|
|
d98fcf8d1f | ||
|
|
5b90b8a3ad | ||
|
|
e343e690f2 | ||
|
|
6f1f4ba98f | ||
|
|
7a34b4de73 | ||
|
|
b84831f148 | ||
|
|
d9ecafeb2a | ||
|
|
930de9aeb7 | ||
|
|
40a09d7412 | ||
|
|
ad9aabbb7e | ||
|
|
195bce0485 | ||
|
|
42b88f34f4 | ||
|
|
7bde7039bb | ||
|
|
4702a0af9d | ||
|
|
c818381ef1 | ||
|
|
ef4ecf7c8e | ||
|
|
fb65cea115 | ||
|
|
dcca79baef | ||
|
|
15df03ba09 | ||
|
|
5eb33e9d8a | ||
|
|
939de5d151 | ||
|
|
4c440b8552 | ||
|
|
1c73fd5454 | ||
|
|
9567ae4bfe | ||
|
|
853b454eae | ||
|
|
94b1fec91e | ||
|
|
a6a5c010fe | ||
|
|
52bcbaed08 | ||
|
|
74ac45730f | ||
|
|
0d0f04de5c | ||
|
|
e1be294147 | ||
|
|
45077d7106 | ||
|
|
e65ead3f20 | ||
|
|
5ed6d0fedb | ||
|
|
673138c5af | ||
|
|
0e145a1a57 | ||
|
|
a8bcfc62d0 | ||
|
|
394bb38913 | ||
|
|
80c5c94e7e | ||
|
|
03bfd2c842 | ||
|
|
bdb438972f | ||
|
|
f256206fd4 | ||
|
|
4d01929503 | ||
|
|
2a9b76246b | ||
|
|
21bcef5008 | ||
|
|
46e7fc61cc | ||
|
|
b9dee59900 | ||
|
|
2fd4c149fd | ||
|
|
d085768e8b | ||
|
|
6f9730c652 | ||
|
|
b79a0a9832 | ||
|
|
212061be50 | ||
|
|
c931341259 | ||
|
|
51dc03a730 | ||
|
|
31670da633 | ||
|
|
51a808c094 | ||
|
|
d756086b8e | ||
|
|
81d289b266 | ||
|
|
71c2551a2e | ||
|
|
ab2c8c7382 | ||
|
|
ba40fe4ff4 | ||
|
|
dbced5c1a4 | ||
|
|
3a1a0d6590 | ||
|
|
451d9aab96 | ||
|
|
a6949d522e | ||
|
|
eba372e2cc | ||
|
|
a3ac0d542d | ||
|
|
0709b0dc3b | ||
|
|
5bb710e921 | ||
|
|
a5c1e594b3 | ||
|
|
08f8f51ade | ||
|
|
3897950e22 | ||
|
|
e9db1ecd1f | ||
|
|
9346eba2f2 | ||
|
|
c5ba1d62e7 | ||
|
|
37996bcd15 | ||
|
|
d5729f9216 | ||
|
|
4cec11be9c | ||
|
|
b80c14639e | ||
|
|
0d3bd2052b | ||
|
|
353a8c5388 | ||
|
|
dac3037ee4 | ||
|
|
e3c81048af | ||
|
|
c752ca84df | ||
|
|
a5d50960a7 | ||
|
|
af0845b4a3 | ||
|
|
8d77b1ae16 | ||
|
|
c185529ad5 | ||
|
|
3a5def98e5 | ||
|
|
603c68bd69 | ||
|
|
2f984964da | ||
|
|
22aaf0471a | ||
|
|
1134d0d7d4 | ||
|
|
45ff3ee637 | ||
|
|
50ba6bfa1b | ||
|
|
cdcbb1d6da | ||
|
|
49bf22ace4 | ||
|
|
22fa95cf87 | ||
|
|
a76a889924 | ||
|
|
8f7c6a635f | ||
|
|
9ece6b1fbc | ||
|
|
9d7a45f459 | ||
|
|
0465ba24a2 | ||
|
|
b622c89967 | ||
|
|
7efb141f88 | ||
|
|
d093912f9d | ||
|
|
92e5c8d228 | ||
|
|
68995e6ed8 | ||
|
|
edac29fe51 | ||
|
|
e7f72a1ba8 | ||
|
|
d0519d6c25 | ||
|
|
2ce435f98d | ||
|
|
67fde3e941 | ||
|
|
1282c060fc | ||
|
|
52f1fc943d | ||
|
|
88ff19383b | ||
|
|
832312e095 | ||
|
|
f2ed01993d | ||
|
|
ccf869e671 | ||
|
|
48f579977e | ||
|
|
ab595659c2 | ||
|
|
0cd2b78425 | ||
|
|
c1e618fc3b | ||
|
|
934b9faf36 | ||
|
|
2836dcb71f | ||
|
|
2b874c5817 | ||
|
|
19b73d2ba7 | ||
|
|
548a17d94c | ||
|
|
de4005d470 | ||
|
|
46e9b9499c | ||
|
|
580b06ada5 | ||
|
|
1472e7b051 | ||
|
|
9817754f92 | ||
|
|
05f1959c83 | ||
|
|
80a692a751 | ||
|
|
a5ebb6d98c | ||
|
|
04c1e4b57d | ||
|
|
f8f9c06ed8 | ||
|
|
1f4b2607cc | ||
|
|
f0c518a134 | ||
|
|
c396518736 | ||
|
|
d6a45b511e | ||
|
|
f5c4c3e450 | ||
|
|
aecfb5772d | ||
|
|
3b7b936582 | ||
|
|
309bdf3d70 | ||
|
|
55f9203f60 | ||
|
|
df7ba37d57 | ||
|
|
96cb4e9a52 | ||
|
|
f070be8dd3 | ||
|
|
3408a0537f | ||
|
|
5669cf832f | ||
|
|
e2ac4b45d0 | ||
|
|
714daba393 | ||
|
|
1913e9512c | ||
|
|
ae72994cfb | ||
|
|
f9b2f3e87d | ||
|
|
e05bf87528 | ||
|
|
7609a5456e | ||
|
|
4809a2e0df | ||
|
|
e61c34b85f | ||
|
|
ba0dc60b09 | ||
|
|
f61d6320a3 | ||
|
|
3ef41f2b1d | ||
|
|
2e6f547bfc | ||
|
|
c66b25b4cc | ||
|
|
a66edbdda8 | ||
|
|
ef0d834db8 | ||
|
|
89daacac32 | ||
|
|
f4f15ee5e9 | ||
|
|
4c36ef9d24 | ||
|
|
886af692e2 | ||
|
|
c63949f384 | ||
|
|
9460e915cd | ||
|
|
34a324f551 | ||
|
|
996694aec4 | ||
|
|
e21060a259 | ||
|
|
951d342ed7 | ||
|
|
74b76704f8 | ||
|
|
6efc37eaf3 | ||
|
|
9cf4e68ac4 | ||
|
|
1c35c8b617 | ||
|
|
94dabe225b | ||
|
|
21b490f03a | ||
|
|
f8f509a296 | ||
|
|
062b771622 | ||
|
|
adff472011 | ||
|
|
9c043dbf9f | ||
|
|
59d28e7b99 | ||
|
|
8b5a133106 | ||
|
|
86a41dd6dd | ||
|
|
2d77b25e26 | ||
|
|
43526c2297 | ||
|
|
192762add8 | ||
|
|
0f64847351 | ||
|
|
88d2f26269 | ||
|
|
21aabb1215 | ||
|
|
e6ea51f8cb | ||
|
|
6d52e1709f | ||
|
|
aaf1fd0256 | ||
|
|
97ef539a6c | ||
|
|
b630aad44a | ||
|
|
2f6fb4b97c | ||
|
|
5c99114b02 | ||
|
|
318421df9d | ||
|
|
2f42dc6473 | ||
|
|
66bcdc3c00 | ||
|
|
8b042caf60 | ||
|
|
b3408f5ad4 | ||
|
|
5558bc1ef3 | ||
|
|
2426a731fc | ||
|
|
47e2b0991a | ||
|
|
ceec98ffc3 | ||
|
|
a24b977f72 | ||
|
|
4602ffba29 | ||
|
|
21d4ee8e2e | ||
|
|
8ec41a7128 | ||
|
|
8842625bb4 | ||
|
|
586be86b41 | ||
|
|
55190fd703 | ||
|
|
64f7978037 | ||
|
|
3f33751ff2 | ||
|
|
392f349da1 | ||
|
|
bbd83b9dba | ||
|
|
2e9b4779c9 | ||
|
|
8475f95b19 | ||
|
|
2c0669c865 | ||
|
|
937b551ae4 | ||
|
|
88a50d3779 | ||
|
|
57be6616ce | ||
|
|
7e073e6e26 | ||
|
|
10ff95af6a | ||
|
|
378d3e3889 | ||
|
|
88f1353670 | ||
|
|
c229aa8cfe | ||
|
|
96549a5421 | ||
|
|
a99c6145a7 | ||
|
|
c85ab8dcb6 | ||
|
|
db172b341e | ||
|
|
11dbba422f | ||
|
|
a1e60045d9 | ||
|
|
b36a9d5af3 | ||
|
|
fb5ff49008 | ||
|
|
1b857f743e | ||
|
|
c2df14cfa0 | ||
|
|
44ff7b0f5f | ||
|
|
5fdbd7428b | ||
|
|
bc9aaebd04 | ||
|
|
d90a0ce910 | ||
|
|
9c19043fed | ||
|
|
beb39ada0c | ||
|
|
47f3eb28a6 | ||
|
|
28105af761 | ||
|
|
e0ac87e702 | ||
|
|
b9ea05f97a | ||
|
|
e348163689 | ||
|
|
90375531bc | ||
|
|
ca85f2d725 | ||
|
|
eaadb75d39 | ||
|
|
f6793df94a | ||
|
|
90e716b932 | ||
|
|
db2853c8ae | ||
|
|
120527cb41 | ||
|
|
058f33cd7b | ||
|
|
88582726b7 | ||
|
|
fd5f5e3204 | ||
|
|
47d7a71ad0 | ||
|
|
88a6be16af | ||
|
|
23cacbf81a | ||
|
|
db304513b1 | ||
|
|
8d1b4bf78f | ||
|
|
1cff37af99 | ||
|
|
ecc7f2d780 | ||
|
|
dbb6610d50 | ||
|
|
ae135ac181 | ||
|
|
df1d68b626 | ||
|
|
d35d12f671 | ||
|
|
cb31179274 | ||
|
|
c9277c3c72 | ||
|
|
5896a05ca6 | ||
|
|
5cf0aadd84 | ||
|
|
a18bac7380 | ||
|
|
9a1c7953e7 | ||
|
|
53c526f48c | ||
|
|
49bfd05d7c | ||
|
|
62ea2c691e | ||
|
|
7de600cc64 | ||
|
|
0c703276b1 | ||
|
|
11ac8da14e | ||
|
|
f736b6c299 | ||
|
|
6f886fe167 | ||
|
|
986baed290 | ||
|
|
84c5f76b76 | ||
|
|
39c6f41f0e | ||
|
|
bdb01e6166 | ||
|
|
35ddcdd266 | ||
|
|
7e8e1215ad | ||
|
|
83abd7ef59 | ||
|
|
26fa6c97bf | ||
|
|
708c6bdeb8 | ||
|
|
c73adf02bc | ||
|
|
c36a468561 | ||
|
|
83794c433f | ||
|
|
eb82346ce7 | ||
|
|
1a6ec0d768 | ||
|
|
446f8f93c6 | ||
|
|
445a801d5d | ||
|
|
579a135087 | ||
|
|
cd1b251cef | ||
|
|
ed23194abd | ||
|
|
2ac0c0a957 | ||
|
|
477b161ba2 | ||
|
|
cb1353c898 | ||
|
|
47dc137de0 | ||
|
|
03c6b0c688 | ||
|
|
ffc5e2f446 | ||
|
|
4ebb61b580 | ||
|
|
4ae25e57a3 | ||
|
|
7bd4f3874e | ||
|
|
2b7080294d | ||
|
|
3b51565a13 | ||
|
|
7400b888df | ||
|
|
3f15a7251b | ||
|
|
a9356fd377 | ||
|
|
f3eb9f79a1 | ||
|
|
93026bcae0 | ||
|
|
6082c00b1d | ||
|
|
e6e181e6bc | ||
|
|
502452f58e | ||
|
|
1d0cceb2d3 | ||
|
|
b75ae0f7b1 | ||
|
|
5f0217cdb0 | ||
|
|
7b40aa89b4 | ||
|
|
e817a7b6c6 | ||
|
|
b53c2e1a26 | ||
|
|
383e2ed46f | ||
|
|
258b084a62 | ||
|
|
c5593bde08 | ||
|
|
2cd4580154 | ||
|
|
2442dc09b0 | ||
|
|
d482ca5d68 | ||
|
|
3616b697ec | ||
|
|
a19dfe608d | ||
|
|
0c1e9b6678 | ||
|
|
391313cd39 | ||
|
|
8fb6ded506 | ||
|
|
eb7b2f4683 | ||
|
|
520918348e | ||
|
|
e7b3910f3d | ||
|
|
7515ed6146 | ||
|
|
901adb38ae | ||
|
|
522378a507 | ||
|
|
1bb2fc912b | ||
|
|
423d112dfc | ||
|
|
63fde03552 | ||
|
|
d1d27b5367 | ||
|
|
0e302baa43 | ||
|
|
a7c2d54e53 | ||
|
|
54743eda16 | ||
|
|
660cf26e49 | ||
|
|
e4875f26ff | ||
|
|
7f1f22146d | ||
|
|
0103c689f7 | ||
|
|
f4a31251cb | ||
|
|
b18ded0e2d | ||
|
|
c327116e7f | ||
|
|
ef071ac7ac | ||
|
|
56990f2a29 | ||
|
|
4475a82776 | ||
|
|
560a30662b | ||
|
|
a066bbe32f | ||
|
|
e252b4b674 | ||
|
|
3283ffb4e1 | ||
|
|
a840db3e79 | ||
|
|
dc19d7c72c | ||
|
|
a95b1b4881 | ||
|
|
4d0fc16fe3 | ||
|
|
b13c3be92f | ||
|
|
a3a222c1f1 | ||
|
|
1d7cfcdd99 | ||
|
|
2c05e721fd | ||
|
|
22acf1ec25 | ||
|
|
9c8deb1470 | ||
|
|
9b9cb5c076 | ||
|
|
0b6a5b7630 | ||
|
|
fac025367b | ||
|
|
e5246f3993 | ||
|
|
89370a1f74 | ||
|
|
51a564eb9c | ||
|
|
b50b6ccb8b | ||
|
|
7a8e2c67b0 | ||
|
|
1b4a3475e8 | ||
|
|
3d2d35f5b6 | ||
|
|
489d8110c9 | ||
|
|
fc5bf99715 | ||
|
|
999bdc9498 | ||
|
|
06e3528bc8 | ||
|
|
b06a88b09f | ||
|
|
51aab3eacc | ||
|
|
545807755d | ||
|
|
b7ae55a07b | ||
|
|
a980b3d132 | ||
|
|
58047e8b82 | ||
|
|
9abeaa74c6 | ||
|
|
c512e48c52 | ||
|
|
8924437fc5 | ||
|
|
1d8e242b3c | ||
|
|
cf33f7c281 | ||
|
|
56ac6f66bf | ||
|
|
5ac649b21a | ||
|
|
4a567c9f5a | ||
|
|
479fddfeec | ||
|
|
cc6f9e9da4 | ||
|
|
de9df12ea9 | ||
|
|
8145829619 | ||
|
|
3bd0cb9a15 | ||
|
|
412d6fe42c | ||
|
|
12362cac61 | ||
|
|
001d26d73c | ||
|
|
75191982da | ||
|
|
8093a5f419 | ||
|
|
3d53847f30 | ||
|
|
bb944f190e | ||
|
|
7d84083f55 | ||
|
|
b591ccc40d | ||
|
|
46b95ed639 | ||
|
|
c050cf305c | ||
|
|
d5bf8966d9 | ||
|
|
49bc27e24f | ||
|
|
6240a5d7a4 | ||
|
|
af67b72492 | ||
|
|
d2145368fa | ||
|
|
4010900e47 | ||
|
|
2d1af70e87 | ||
|
|
76d4de225a | ||
|
|
f5c3b00c42 | ||
|
|
f9fddfc43d | ||
|
|
12b912140b | ||
|
|
56f4c3d6ae | ||
|
|
f560d187c1 | ||
|
|
d4d7446789 | ||
|
|
663da772e0 | ||
|
|
b51226f76c | ||
|
|
297ea529a2 | ||
|
|
016d176cbd | ||
|
|
83e582b7ab | ||
|
|
fbeb45d1da | ||
|
|
be24cdb917 | ||
|
|
685d14c4d5 | ||
|
|
c50cd4cc3e | ||
|
|
e240160d39 | ||
|
|
dae8c3599b | ||
|
|
7d8c0268ea | ||
|
|
fc2df6cfd6 | ||
|
|
5b2ccdbda7 | ||
|
|
a0d724c186 | ||
|
|
aec2baa7e7 | ||
|
|
67bbbd8671 | ||
|
|
3e1dbd4c39 | ||
|
|
df4b07f9f2 | ||
|
|
bd4c2da37c | ||
|
|
1c06e576ae | ||
|
|
5d3629173f | ||
|
|
5f25f40db9 | ||
|
|
a4efefbf5e | ||
|
|
56e77d3106 | ||
|
|
2a9e16bb8e | ||
|
|
c512485514 | ||
|
|
a0fb880ebc | ||
|
|
e52eafa7bb | ||
|
|
9f8ea28db4 | ||
|
|
1c1eaa6296 | ||
|
|
b32bfedde9 | ||
|
|
f7d36a670c | ||
|
|
37461c2a5a | ||
|
|
5a17825b47 | ||
|
|
761c25fa6d | ||
|
|
c40df65d13 | ||
|
|
79860e7b15 | ||
|
|
ca68a38f2c | ||
|
|
b075cc5e6f | ||
|
|
3e3c51ae53 | ||
|
|
28d7eddcb4 | ||
|
|
509327b4c7 | ||
|
|
ee74130806 | ||
|
|
0edbbf9fe0 | ||
|
|
7ec658a3f8 | ||
|
|
2b4da1f88e | ||
|
|
84608256ea | ||
|
|
0f8a034e8b | ||
|
|
0631323bf5 | ||
|
|
825cc006e5 | ||
|
|
f1e01c7166 | ||
|
|
15e3173177 | ||
|
|
34bf2f9a0f | ||
|
|
71a78cbf01 | ||
|
|
03fc6c3f97 | ||
|
|
e6c095754f | ||
|
|
d045d39f88 | ||
|
|
63bf719067 | ||
|
|
34168bd223 | ||
|
|
4f3e3d9285 | ||
|
|
c41f1c3d22 | ||
|
|
7d8086e138 | ||
|
|
0727797254 | ||
|
|
7a8c3a42cd | ||
|
|
84f5f20edb | ||
|
|
b80b5a94f9 | ||
|
|
a43cc23d67 | ||
|
|
9e1f607bd9 | ||
|
|
914861503b | ||
|
|
8e996ae571 | ||
|
|
5d351ec8c0 | ||
|
|
33db0203dd | ||
|
|
7a1bef7daf | ||
|
|
2bbda38488 | ||
|
|
e3e59c35fc | ||
|
|
dc61fae12a | ||
|
|
dada7fe728 | ||
|
|
bff74b1d7d | ||
|
|
224fb5c6a2 | ||
|
|
05e1cd8478 | ||
|
|
a2297a5fa6 | ||
|
|
94f5c37884 | ||
|
|
ed334fd790 | ||
|
|
efb2069500 | ||
|
|
924526c1e6 | ||
|
|
89995fe7ca | ||
|
|
929914da5d | ||
|
|
31669cbad6 | ||
|
|
a1e73331b8 | ||
|
|
31da88ad89 | ||
|
|
93cdee3bab | ||
|
|
ed7d8e7c3b | ||
|
|
050d9ff04d | ||
|
|
512076898d | ||
|
|
a628e427de | ||
|
|
05f1887559 | ||
|
|
266859edf3 | ||
|
|
79ea49cafd | ||
|
|
3cb67f2806 | ||
|
|
fa4de08934 | ||
|
|
70c8f8733a | ||
|
|
97dc61e65f | ||
|
|
48aebec01f | ||
|
|
58078459f8 | ||
|
|
fb9b19b95e | ||
|
|
7af6cf450b | ||
|
|
ca12cf5802 | ||
|
|
d7737f6c88 | ||
|
|
b5fdd3b8d8 | ||
|
|
5b95f7e565 | ||
|
|
be3f3da532 | ||
|
|
f13e52fffb | ||
|
|
ab66cb5d26 | ||
|
|
878b762537 | ||
|
|
c5e144f327 | ||
|
|
92ac99e878 | ||
|
|
70dabf11db | ||
|
|
0f83d645b6 | ||
|
|
794573969c | ||
|
|
303abf8a91 | ||
|
|
43303ba4c1 | ||
|
|
31ccb796ac | ||
|
|
3395ccb943 | ||
|
|
c630b92772 | ||
|
|
903d75e6d3 | ||
|
|
0be6d9f048 | ||
|
|
a574591481 | ||
|
|
435d480cf1 | ||
|
|
3969eb9382 | ||
|
|
45af02590e | ||
|
|
709214a414 | ||
|
|
6dda0c2b97 | ||
|
|
20578e0a43 | ||
|
|
5a1e8484a9 | ||
|
|
1093b7c3d3 | ||
|
|
7ec99c2469 | ||
|
|
3543155953 | ||
|
|
e2df7dc96c | ||
|
|
cd5f3e9740 | ||
|
|
ae70ef0fa2 | ||
|
|
f1a17e3cf5 | ||
|
|
22a2496360 | ||
|
|
7a7f3466f3 | ||
|
|
73f9d45fdc | ||
|
|
3cba961dad | ||
|
|
6710c13cc2 | ||
|
|
f12120b055 | ||
|
|
693f30477e | ||
|
|
92709110ef | ||
|
|
d81b4999dc | ||
|
|
f088a9953f | ||
|
|
a069df7a36 | ||
|
|
7f26b04ea1 | ||
|
|
f81cca67eb | ||
|
|
aca6d24e96 | ||
|
|
25bbde2be2 | ||
|
|
c7ad4aa460 | ||
|
|
9c9aa68c23 | ||
|
|
6cc6cb5ffd | ||
|
|
7ad2a68052 | ||
|
|
8991ed8270 | ||
|
|
45d1139ab0 | ||
|
|
26052a2963 | ||
|
|
eeb9270882 | ||
|
|
8fc2d22b28 | ||
|
|
d09e2fbcec | ||
|
|
2797faae6f | ||
|
|
a1906347b0 | ||
|
|
8924aa736e | ||
|
|
f93939ef20 | ||
|
|
700c4acf75 | ||
|
|
f18a6fe1eb | ||
|
|
b07a6d10f1 | ||
|
|
ac6c8a5bd3 | ||
|
|
b3d95d20dc | ||
|
|
28c43ac2bf | ||
|
|
fdfadfefbc | ||
|
|
e57cb8bf7e | ||
|
|
ec78fc7e7c | ||
|
|
cf082cca04 | ||
|
|
1e877a0f96 | ||
|
|
3e2e4ace48 | ||
|
|
0c5962c538 | ||
|
|
98cc17df24 | ||
|
|
2cad586ccc | ||
|
|
da3ae79c37 | ||
|
|
38f05dabe2 | ||
|
|
d0689abd35 | ||
|
|
50b7fcb90a | ||
|
|
7e01c833be | ||
|
|
308c8ba1a5 | ||
|
|
477d1b2569 | ||
|
|
5c7a14cfe0 | ||
|
|
fb9019e0c8 | ||
|
|
b3750bed2d | ||
|
|
fa12649642 | ||
|
|
aacc9c86b9 | ||
|
|
8b2d85c823 | ||
|
|
66709e6f22 | ||
|
|
f34edc3116 | ||
|
|
9cd80a2c6b | ||
|
|
2de22f9356 | ||
|
|
2e237cc449 | ||
|
|
bb734706fc | ||
|
|
09ede0221a | ||
|
|
f859bffa74 | ||
|
|
bca90245c8 | ||
|
|
fe3520c685 | ||
|
|
31f812cf94 | ||
|
|
57a8255654 | ||
|
|
a71ed7b5b7 | ||
|
|
3ba6ffe5f4 | ||
|
|
2571a0654f |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:10
|
FROM node:14
|
||||||
|
|
||||||
# Configure apt
|
# Configure apt
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"dockerFile": "Dockerfile",
|
"dockerFile": "Dockerfile",
|
||||||
"appPort": 3000,
|
"appPort": 3000,
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"editorconfig.editorconfig",
|
"dbaeumer.vscode-eslint",
|
||||||
"ms-vscode.vscode-typescript-tslint-plugin"
|
"editorconfig.editorconfig"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
node_modules/
|
|
||||||
*.swp
|
|
||||||
.lock-wscript
|
|
||||||
lib/
|
|
||||||
Makefile.gyp
|
|
||||||
*.Makefile
|
|
||||||
*.target.gyp.mk
|
|
||||||
*.node
|
|
||||||
example/*.log
|
|
||||||
docs/
|
|
||||||
npm-debug.log
|
|
||||||
/.idea/
|
|
||||||
.env
|
|
||||||
build/
|
|
||||||
.vscode/
|
|
||||||
.git/
|
|
||||||
+177
@@ -0,0 +1,177 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es6": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"src/browser/tsconfig.json",
|
||||||
|
"src/common/tsconfig.json",
|
||||||
|
"test/api/tsconfig.json",
|
||||||
|
"test/benchmark/tsconfig.json",
|
||||||
|
"addons/xterm-addon-attach/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-attach/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-fit/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-fit/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-ligatures/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-search/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-search/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-serialize/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-serialize/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-serialize/benchmark/tsconfig.json",
|
||||||
|
"addons/xterm-addon-unicode11/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-unicode11/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-web-links/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-web-links/test/tsconfig.json",
|
||||||
|
"addons/xterm-addon-webgl/src/tsconfig.json",
|
||||||
|
"addons/xterm-addon-webgl/test/tsconfig.json"
|
||||||
|
],
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/typings/*.d.ts",
|
||||||
|
"**/node_modules",
|
||||||
|
"**/*.js"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"no-extra-semi": "error",
|
||||||
|
"@typescript-eslint/array-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"default": "array",
|
||||||
|
"readonly": "generic"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/consistent-type-assertions": "warn",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "warn",
|
||||||
|
"@typescript-eslint/explicit-function-return-type": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowExpressions": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/explicit-member-accessibility": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"accessibility": "explicit",
|
||||||
|
"overrides": {
|
||||||
|
"constructors": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/indent": [
|
||||||
|
"warn",
|
||||||
|
2
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-delimiter-style": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"multiline": {
|
||||||
|
"delimiter": "semi",
|
||||||
|
"requireLast": true
|
||||||
|
},
|
||||||
|
"singleline": {
|
||||||
|
"delimiter": "comma",
|
||||||
|
"requireLast": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"warn",
|
||||||
|
{ "selector": "default", "format": ["camelCase"] },
|
||||||
|
// variableLike
|
||||||
|
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
|
||||||
|
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
|
||||||
|
// memberLike
|
||||||
|
{ "selector": "memberLike", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||||
|
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||||
|
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
||||||
|
// memberLike - Allow enum-like objects to use UPPER_CASE
|
||||||
|
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||||
|
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||||
|
// typeLike
|
||||||
|
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||||
|
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||||
|
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||||
|
"@typescript-eslint/quotes": [
|
||||||
|
"warn",
|
||||||
|
"single",
|
||||||
|
{ "allowTemplateLiterals": true }
|
||||||
|
],
|
||||||
|
"@typescript-eslint/semi": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"comma-dangle": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"objects": "never",
|
||||||
|
"arrays": "never",
|
||||||
|
"functions": "never"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"curly": [
|
||||||
|
"warn",
|
||||||
|
"multi-line"
|
||||||
|
],
|
||||||
|
"eol-last": "warn",
|
||||||
|
"eqeqeq": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"keyword-spacing": "warn",
|
||||||
|
"new-parens": "warn",
|
||||||
|
"no-duplicate-imports": "warn",
|
||||||
|
"no-else-return": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"allowElseIf": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-eval": "warn",
|
||||||
|
"no-irregular-whitespace": "warn",
|
||||||
|
"no-restricted-imports": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"patterns": [
|
||||||
|
".*\\/out\\/.*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"no-trailing-spaces": "warn",
|
||||||
|
"no-unsafe-finally": "warn",
|
||||||
|
"no-var": "warn",
|
||||||
|
"one-var": [
|
||||||
|
"warn",
|
||||||
|
"never"
|
||||||
|
],
|
||||||
|
"object-curly-spacing": [
|
||||||
|
"warn",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"prefer-const": "warn",
|
||||||
|
"spaced-comment": [
|
||||||
|
"warn",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"markers": ["/"],
|
||||||
|
"exceptions": ["-"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["**/*.test.ts"],
|
||||||
|
"rules": {
|
||||||
|
"object-curly-spacing": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -42,7 +42,6 @@
|
|||||||
*.test.js
|
*.test.js
|
||||||
*.test.js.map
|
*.test.js.map
|
||||||
lib/test/
|
lib/test/
|
||||||
tslint.json
|
|
||||||
webpack.config.js
|
webpack.config.js
|
||||||
|
|
||||||
docs/
|
docs/
|
||||||
|
|||||||
Vendored
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
"typescript.preferences.quoteStyle": "single"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+68
-49
@@ -1,51 +1,70 @@
|
|||||||
{
|
{
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"echo": false,
|
"echo": false,
|
||||||
"reveal": "always",
|
"reveal": "always",
|
||||||
"focus": false,
|
"focus": false,
|
||||||
"panel": "dedicated",
|
"panel": "dedicated",
|
||||||
"showReuseMessage": true
|
"showReuseMessage": true
|
||||||
},
|
},
|
||||||
"tasks": [
|
"tasks": [
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "test",
|
"script": "test",
|
||||||
"group": "test",
|
"group": "test",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "watch",
|
"script": "watch",
|
||||||
"group": "build",
|
"group": "build",
|
||||||
"isBackground": true,
|
"isBackground": true,
|
||||||
"problemMatcher": [],
|
"problemMatcher": "$tsc-watch",
|
||||||
"presentation": {
|
"presentation": {
|
||||||
"group": "vscode"
|
"group": "vscode"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "npm",
|
"type": "npm",
|
||||||
"script": "start",
|
"script": "start",
|
||||||
"group": "build",
|
"dependsOn": "npm: watch",
|
||||||
"isBackground": true,
|
"group": "build",
|
||||||
"problemMatcher": [],
|
"isBackground": true,
|
||||||
"presentation": {
|
"problemMatcher": [],
|
||||||
"group": "vscode"
|
"presentation": {
|
||||||
}
|
"group": "vscode"
|
||||||
},
|
}
|
||||||
{
|
},
|
||||||
"label": "Start demo",
|
{
|
||||||
"dependsOn": ["npm: watch", "npm: start"],
|
"label": "Start demo",
|
||||||
"group": {
|
"dependsOn": "npm: start",
|
||||||
"kind": "build",
|
"group": {
|
||||||
"isDefault": true
|
"kind": "build",
|
||||||
},
|
"isDefault": true
|
||||||
"isBackground": true,
|
},
|
||||||
"problemMatcher": [],
|
"isBackground": true,
|
||||||
"presentation": {
|
"problemMatcher": {
|
||||||
"group": "vscode"
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
FROM node:8
|
|
||||||
MAINTAINER Paris Kasidiaris <paris@sourcelair.com>
|
|
||||||
|
|
||||||
# Set the working directory
|
|
||||||
WORKDIR /usr/src/app
|
|
||||||
|
|
||||||
# Set an entrypoint, to automatically install node modules
|
|
||||||
ENTRYPOINT ["/bin/bash", "-c", "if [[ ! -d node_modules ]]; then npm install; fi; exec \"${@:0}\";"]
|
|
||||||
CMD ["npm", "run", "start"]
|
|
||||||
|
|
||||||
# First, install dependencies to improve layer caching
|
|
||||||
COPY package.json /usr/src/app/
|
|
||||||
RUN npm install
|
|
||||||
|
|
||||||
# Add the code
|
|
||||||
COPY . /usr/src/app
|
|
||||||
|
|
||||||
# Run the tests and build, to make sure everything is working nicely
|
|
||||||
RUN npm run build && npm run test
|
|
||||||
@@ -6,11 +6,11 @@ Xterm.js is a front-end component written in TypeScript that lets applications b
|
|||||||
|
|
||||||
## Features
|
## 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.
|
- **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.
|
- **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.
|
- **And much more**: Links, theming, addons, well documented API, etc.
|
||||||
|
|
||||||
## What xterm.js is not
|
## 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
|
## 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
|
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
|
```html
|
||||||
<!doctype 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*
|
⚠️ *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
|
```bash
|
||||||
npm i -S xterm-addon-web-links
|
npm i -S xterm-addon-web-links
|
||||||
@@ -76,7 +76,7 @@ const terminal = new Terminal();
|
|||||||
terminal.loadAddon(new WebLinksAddon());
|
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-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
|
- [`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,23 @@ The xterm.js team maintains the following addons but they can be built by anyone
|
|||||||
|
|
||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox* and *Safari*.
|
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
|
||||||
|
|
||||||
We also partially support *Intenet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
||||||
|
|
||||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers, these are the versions we strive to keep working.
|
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
|
||||||
|
|
||||||
## API
|
## 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.
|
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
|
## Real-world uses
|
||||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||||
|
|
||||||
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
- [**SourceLair**](https://www.sourcelair.com/): In-browser IDE that provides its users with fully-featured Linux terminals based on xterm.js.
|
||||||
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
- [**Microsoft Visual Studio Code**](http://code.visualstudio.com/): Modern, versatile, and powerful open source code editor that provides an integrated terminal based on xterm.js.
|
||||||
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
|
- [**ttyd**](https://github.com/tsl0922/ttyd): A command-line tool for sharing terminal over the web, with fully-featured terminal emulation based on xterm.js.
|
||||||
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
- [**Katacoda**](https://www.katacoda.com/): Katacoda is an Interactive Learning Platform for software developers, covering the latest Cloud Native technologies.
|
||||||
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
- [**Eclipse Che**](http://www.eclipse.org/che): Developer workspace server, cloud IDE, and Eclipse next-generation IDE.
|
||||||
@@ -113,10 +113,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.
|
- [**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.
|
- [**RStudio**](https://www.rstudio.com/products/RStudio "RStudio"): RStudio is an integrated development environment (IDE) for R.
|
||||||
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
- [**Terminal for Atom**](https://github.com/jsmecham/atom-terminal-tab): A simple terminal for the Atom text editor.
|
||||||
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy and run in the cloud.
|
- [**Eclipse Orion**](https://orionhub.org): A modern, open source software development environment that runs in the cloud. Code, deploy, and run in the cloud.
|
||||||
- [**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.
|
- [**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.
|
- [**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.
|
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
|
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising xterm.js, SJCL & websockets.
|
||||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
- [**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 +143,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.
|
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
|
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
|
||||||
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
- [**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.
|
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
|
||||||
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
|
- [**webssh**](https://github.com/huashengdun/webssh): Web based ssh client.
|
||||||
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
|
- [**info-beamer hosted**](https://info-beamer.com): Uses xterm.js to manage digital signage devices from the web dashboard.
|
||||||
@@ -156,10 +155,30 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
|||||||
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
|
- [**tty-share**](https://tty-share.com): Extremely simple terminal sharing over the Internet.
|
||||||
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
|
- [**Ten Hands**](https://github.com/saisandeepvaddi/ten-hands): One place to run your command-line tasks.
|
||||||
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
|
- [**WebAssembly.sh**](https://webassembly.sh): A WebAssembly WASI browser terminal
|
||||||
|
- [**Gus**](https://gus.jp): A shared coding pad where you can run Python with xterm.js
|
||||||
|
- [**Linode**](https://linode.com): Linode uses xterm.js to provide users a web console for their Linode instances.
|
||||||
|
- [**FluffOS**](https://www.fluffos.info): Active maintained LPMUD driver with websocket support.
|
||||||
|
- [**x-terminal**](https://atom.io/packages/x-terminal): Atom plugin for providing terminals inside your Atom workspace.
|
||||||
|
- [**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.
|
||||||
|
- [**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 are supported, with results displayed by xterm.js.
|
||||||
|
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
|
||||||
|
- [**Commas**](https://github.com/CyanSalt/commas): Commas is a hackable terminal and command runner.
|
||||||
|
- [**Devtron**](https://github.com/devtron-labs/devtron): Software Delivery Workflow For Kubernetes.
|
||||||
|
- [**NxShell**](https://github.com/nxshell/nxshell): An easy to use new terminal for SSH.
|
||||||
|
- [**gifcast**](https://dstein64.github.io/gifcast/): Converts an asciinema cast to an animated GIF.
|
||||||
|
- [**WizardWebssh**](https://gitlab.com/mikeramsey/wizardwebssh): A terminal with Pyqt5 Widget for embedding, which can be used as an ssh client to connect to your ssh servers. It is written in Python, based on tornado, paramiko, and xterm.js.
|
||||||
|
- [**Wizard Assistant**](https://wizardassistant.com/): Wizard Assistant comes with advanced automation tools, preloaded common and special time-saving commands, and a built-in SSH terminal. Now you can remotely administer, troubleshoot, and analyze any system with ease.
|
||||||
|
- [**ucli**](https://github.com/tsadarsh/ucli): Command Line for everyone :family_man_woman_girl_boy: at [www.ucli.tech](https://www.ucli.tech).
|
||||||
|
- [**Tess**](https://github.com/SquitchYT/Tess/): Simple Terminal Fully Customizable for Everyone.
|
||||||
|
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||||
|
|
||||||
[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 on our list. Note: Please add any new contributions to the end of the list only.
|
||||||
|
|
||||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
|
||||||
|
|
||||||
## Releases
|
## Releases
|
||||||
|
|
||||||
@@ -169,21 +188,21 @@ All current and past releases are available on this repo's [Releases page](https
|
|||||||
|
|
||||||
### Beta builds
|
### 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
|
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
|
## 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
|
## 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) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-attach",
|
"name": "xterm-addon-attach",
|
||||||
"version": "0.4.0",
|
"version": "0.6.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../../node_modules/.bin/tsc -p src",
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as puppeteer from 'puppeteer';
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { ITerminalOptions } from 'xterm';
|
|
||||||
import WebSocket = require('ws');
|
|
||||||
|
|
||||||
const APP = 'http://127.0.0.1:3000/test';
|
|
||||||
|
|
||||||
let browser: puppeteer.Browser;
|
|
||||||
let page: puppeteer.Page;
|
|
||||||
const width = 800;
|
|
||||||
const height = 600;
|
|
||||||
|
|
||||||
describe('AttachAddon', () => {
|
|
||||||
before(async function(): Promise<any> {
|
|
||||||
this.timeout(20000);
|
|
||||||
browser = await puppeteer.launch({
|
|
||||||
headless: process.argv.indexOf('--headless') !== -1,
|
|
||||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
|
||||||
});
|
|
||||||
page = (await browser.pages())[0];
|
|
||||||
await page.setViewport({ width, height });
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await browser.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async function(): Promise<any> {
|
|
||||||
this.timeout(20000);
|
|
||||||
await page.goto(APP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('string', async function(): Promise<any> {
|
|
||||||
this.timeout(20000);
|
|
||||||
await openTerminal({ rendererType: 'dom' });
|
|
||||||
const port = 8080;
|
|
||||||
const server = new WebSocket.Server({ port });
|
|
||||||
server.on('connection', socket => socket.send('foo'));
|
|
||||||
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
|
||||||
await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('utf8', async function(): Promise<any> {
|
|
||||||
this.timeout(20000);
|
|
||||||
await openTerminal({ rendererType: 'dom' });
|
|
||||||
const port = 8080;
|
|
||||||
const server = new WebSocket.Server({ port });
|
|
||||||
const data = new Uint8Array([102, 111, 111]);
|
|
||||||
server.on('connection', socket => socket.send(data));
|
|
||||||
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
|
||||||
await pollFor(page, `window.term.buffer.getLine(0).translateToString(true)`, 'foo');
|
|
||||||
server.close();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
|
||||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
|
||||||
if (options.rendererType === 'dom') {
|
|
||||||
await page.waitForSelector('.xterm-rows');
|
|
||||||
} else {
|
|
||||||
await page.waitForSelector('.xterm-text-layer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollFor(page: puppeteer.Page, fn: string, val: any): Promise<void> {
|
|
||||||
const result = await page.evaluate(fn);
|
|
||||||
if (result !== val) {
|
|
||||||
return new Promise<void>(r => {
|
|
||||||
setTimeout(() => r(pollFor(page, fn, val)), 10);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,7 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
this._socket = socket;
|
this._socket = socket;
|
||||||
// always set binary type to arraybuffer, we do not handle blobs
|
// always set binary type to arraybuffer, we do not handle blobs
|
||||||
this._socket.binaryType = 'arraybuffer';
|
this._socket.binaryType = 'arraybuffer';
|
||||||
this._bidirectional = (options && options.bidirectional === false) ? false : true;
|
this._bidirectional = !(options && options.bidirectional === false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
@@ -41,7 +41,9 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this._disposables.forEach(d => d.dispose());
|
for (const d of this._disposables) {
|
||||||
|
d.dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sendData(data: string): void {
|
private _sendData(data: string): void {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2015",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"es2015"
|
"es2015"
|
||||||
@@ -10,7 +10,10 @@
|
|||||||
"outDir": "../out",
|
"outDir": "../out",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import WebSocket = require('ws');
|
||||||
|
import { openTerminal, pollFor, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||||
|
import { Browser, Page } from 'playwright';
|
||||||
|
|
||||||
|
const APP = 'http://127.0.0.1:3001/test';
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
let page: Page;
|
||||||
|
const width = 800;
|
||||||
|
const height = 600;
|
||||||
|
|
||||||
|
describe('AttachAddon', () => {
|
||||||
|
before(async function(): Promise<any> {
|
||||||
|
const browserType = getBrowserType();
|
||||||
|
browser = await browserType.launch({
|
||||||
|
headless: process.argv.includes('--headless')
|
||||||
|
});
|
||||||
|
page = await (await browser.newContext()).newPage();
|
||||||
|
await page.setViewportSize({ width, height });
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => await page.goto(APP));
|
||||||
|
|
||||||
|
it('string', async function(): Promise<any> {
|
||||||
|
await openTerminal(page, { rendererType: 'dom' });
|
||||||
|
const port = 8080;
|
||||||
|
const server = new WebSocket.Server({ port });
|
||||||
|
server.on('connection', socket => socket.send('foo'));
|
||||||
|
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
||||||
|
await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('utf8', async function(): Promise<any> {
|
||||||
|
await openTerminal(page, { rendererType: 'dom' });
|
||||||
|
const port = 8080;
|
||||||
|
const server = new WebSocket.Server({ port });
|
||||||
|
const data = new Uint8Array([102, 111, 111]);
|
||||||
|
server.on('connection', socket => socket.send(data));
|
||||||
|
await page.evaluate(`window.term.loadAddon(new window.AttachAddon(new WebSocket('ws://localhost:${port}')))`);
|
||||||
|
await pollFor(page, `window.term.buffer.active.getLine(0).translateToString(true)`, 'foo');
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-fit",
|
"name": "xterm-addon-fit",
|
||||||
"version": "0.3.0",
|
"version": "0.5.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../../node_modules/.bin/tsc -p src",
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
import * as puppeteer from 'puppeteer';
|
|
||||||
import { assert } from 'chai';
|
|
||||||
import { ITerminalOptions } from 'xterm';
|
|
||||||
|
|
||||||
const APP = 'http://127.0.0.1:3000/test';
|
|
||||||
|
|
||||||
let browser: puppeteer.Browser;
|
|
||||||
let page: puppeteer.Page;
|
|
||||||
const width = 1024;
|
|
||||||
const height = 768;
|
|
||||||
|
|
||||||
describe('FitAddon', () => {
|
|
||||||
before(async function(): Promise<any> {
|
|
||||||
this.timeout(20000);
|
|
||||||
browser = await puppeteer.launch({
|
|
||||||
headless: process.argv.indexOf('--headless') !== -1,
|
|
||||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
|
||||||
});
|
|
||||||
page = (await browser.pages())[0];
|
|
||||||
await page.setViewport({ width, height });
|
|
||||||
await page.goto(APP);
|
|
||||||
await openTerminal();
|
|
||||||
});
|
|
||||||
|
|
||||||
after(async () => {
|
|
||||||
await browser.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('no terminal', async function(): Promise<any> {
|
|
||||||
await page.evaluate(`window.fit = new FitAddon();`);
|
|
||||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('proposeDimensions', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
return unloadFit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('default', async function(): Promise<any> {
|
|
||||||
await loadFit();
|
|
||||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
|
||||||
cols: 87,
|
|
||||||
rows: 26
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('width', async function(): Promise<any> {
|
|
||||||
await loadFit(1008);
|
|
||||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
|
||||||
cols: 110,
|
|
||||||
rows: 26
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('small', async function(): Promise<any> {
|
|
||||||
await loadFit(1, 1);
|
|
||||||
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
|
||||||
cols: 2,
|
|
||||||
rows: 1
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fit', () => {
|
|
||||||
afterEach(async () => {
|
|
||||||
return unloadFit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('default', async function(): Promise<any> {
|
|
||||||
await loadFit();
|
|
||||||
await page.evaluate(`window.fit.fit()`);
|
|
||||||
assert.equal(await page.evaluate(`window.term.cols`), 87);
|
|
||||||
assert.equal(await page.evaluate(`window.term.rows`), 26);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('width', async function(): Promise<any> {
|
|
||||||
await loadFit(1008);
|
|
||||||
await page.evaluate(`window.fit.fit()`);
|
|
||||||
assert.equal(await page.evaluate(`window.term.cols`), 110);
|
|
||||||
assert.equal(await page.evaluate(`window.term.rows`), 26);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('small', async function(): Promise<any> {
|
|
||||||
await loadFit(1, 1);
|
|
||||||
await page.evaluate(`window.fit.fit()`);
|
|
||||||
assert.equal(await page.evaluate(`window.term.cols`), 2);
|
|
||||||
assert.equal(await page.evaluate(`window.term.rows`), 1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
|
|
||||||
await page.evaluate(`
|
|
||||||
window.fit = new FitAddon();
|
|
||||||
window.term.loadAddon(window.fit);
|
|
||||||
document.querySelector('#terminal-container').style.width='${width}px';
|
|
||||||
document.querySelector('#terminal-container').style.height='${height}px';
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function unloadFit(): Promise<void> {
|
|
||||||
await page.evaluate(`window.fit.dispose();`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
|
||||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
|
||||||
if (options.rendererType === 'dom') {
|
|
||||||
await page.waitForSelector('.xterm-rows');
|
|
||||||
} else {
|
|
||||||
await page.waitForSelector('.xterm-text-layer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@ export class FitAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove reliance on private API
|
// TODO: Remove reliance on private API
|
||||||
const core = (<any>this._terminal)._core;
|
const core = (this._terminal as any)._core;
|
||||||
|
|
||||||
// Force a full render
|
// Force a full render
|
||||||
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
|
if (this._terminal.rows !== dims.rows || this._terminal.cols !== dims.cols) {
|
||||||
@@ -57,7 +57,11 @@ export class FitAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove reliance on private API
|
// TODO: Remove reliance on private API
|
||||||
const core = (<any>this._terminal)._core;
|
const core = (this._terminal as any)._core;
|
||||||
|
|
||||||
|
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
const parentElementStyle = window.getComputedStyle(this._terminal.element.parentElement);
|
||||||
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
const parentElementHeight = parseInt(parentElementStyle.getPropertyValue('height'));
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"outDir": "../out",
|
"outDir": "../out",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||||
|
import { Browser, Page } from 'playwright';
|
||||||
|
|
||||||
|
const APP = 'http://127.0.0.1:3001/test';
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
let page: Page;
|
||||||
|
const width = 1024;
|
||||||
|
const height = 768;
|
||||||
|
|
||||||
|
describe('FitAddon', () => {
|
||||||
|
before(async function(): Promise<any> {
|
||||||
|
const browserType = getBrowserType();
|
||||||
|
browser = await browserType.launch({
|
||||||
|
headless: process.argv.includes('--headless')
|
||||||
|
});
|
||||||
|
page = await (await browser.newContext()).newPage();
|
||||||
|
await page.setViewportSize({ width, height });
|
||||||
|
await page.goto(APP);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async function(): Promise<any> {
|
||||||
|
await page.evaluate(`document.querySelector('#terminal-container').style.display=''`);
|
||||||
|
await openTerminal(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function(): Promise<any> {
|
||||||
|
await page.evaluate(`window.term.dispose()`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('no terminal', async function(): Promise<any> {
|
||||||
|
await page.evaluate(`window.fit = new FitAddon();`);
|
||||||
|
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('proposeDimensions', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
return await unloadFit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default', async function(): Promise<any> {
|
||||||
|
await loadFit();
|
||||||
|
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
|
||||||
|
assert.equal(dimensions.cols, 87);
|
||||||
|
assert.isAbove(dimensions.rows, 24);
|
||||||
|
assert.isBelow(dimensions.rows, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('width', async function(): Promise<any> {
|
||||||
|
await loadFit(1008);
|
||||||
|
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
|
||||||
|
assert.equal(dimensions.cols, 110);
|
||||||
|
assert.isAbove(dimensions.rows, 24);
|
||||||
|
assert.isBelow(dimensions.rows, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('small', async function(): Promise<any> {
|
||||||
|
await loadFit(1, 1);
|
||||||
|
assert.deepEqual(await page.evaluate(`window.fit.proposeDimensions()`), {
|
||||||
|
cols: 2,
|
||||||
|
rows: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hidden', async function(): Promise<any> {
|
||||||
|
await page.evaluate(`window.term.dispose()`);
|
||||||
|
await page.evaluate(`document.querySelector('#terminal-container').style.display='none'`);
|
||||||
|
await page.evaluate(`window.term = new Terminal()`);
|
||||||
|
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||||
|
await loadFit();
|
||||||
|
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fit', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
return await unloadFit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default', async function(): Promise<any> {
|
||||||
|
await loadFit();
|
||||||
|
await page.evaluate(`window.fit.fit()`);
|
||||||
|
const cols: number = await page.evaluate(`window.term.cols`);
|
||||||
|
const rows: number = await page.evaluate(`window.term.rows`);
|
||||||
|
assert.equal(cols, 87);
|
||||||
|
assert.isAbove(rows, 24);
|
||||||
|
assert.isBelow(rows, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('width', async function(): Promise<any> {
|
||||||
|
await loadFit(1008);
|
||||||
|
await page.evaluate(`window.fit.fit()`);
|
||||||
|
const cols: number = await page.evaluate(`window.term.cols`);
|
||||||
|
const rows: number = await page.evaluate(`window.term.rows`);
|
||||||
|
assert.equal(cols, 110);
|
||||||
|
assert.isAbove(rows, 24);
|
||||||
|
assert.isBelow(rows, 29);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('small', async function(): Promise<any> {
|
||||||
|
await loadFit(1, 1);
|
||||||
|
await page.evaluate(`window.fit.fit()`);
|
||||||
|
assert.equal(await page.evaluate(`window.term.cols`), 2);
|
||||||
|
assert.equal(await page.evaluate(`window.term.rows`), 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadFit(width: number = 800, height: number = 450): Promise<void> {
|
||||||
|
await page.evaluate(`
|
||||||
|
window.fit = new FitAddon();
|
||||||
|
window.term.loadAddon(window.fit);
|
||||||
|
document.querySelector('#terminal-container').style.width='${width}px';
|
||||||
|
document.querySelector('#terminal-container').style.height='${height}px';
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function unloadFit(): Promise<void> {
|
||||||
|
await page.evaluate(`window.fit.dispose();`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
node_modules/
|
||||||
|
.nyc_output/
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
lib/
|
||||||
|
fonts/
|
||||||
|
|
||||||
|
.env
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.tgz
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||||
|
*
|
||||||
|
!*/
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# Whitelist - src/
|
||||||
|
!src/**/*.ts
|
||||||
|
!src/**/*.d.ts
|
||||||
|
|
||||||
|
!src/**/*.js
|
||||||
|
!src/**/*.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
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
## xterm-addon-ligatures
|
||||||
|
|
||||||
|
Add support for programming ligatures to [xterm.js] when running in environments with access to [Node.js] APIs (such as [Electron]).
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
* [Node.js] 8.x or higher (present in [Electron] 1.8.3 or higher)
|
||||||
|
* [xterm.js] 4.0.0 or higher using the default canvas renderer
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save xterm-addon-ligatures
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { LigaturesAddon } from 'xterm-addon-ligatures';
|
||||||
|
|
||||||
|
const terminal = new Terminal();
|
||||||
|
const ligaturesAddon = new LigaturesAddon();
|
||||||
|
terminal.open(containerElement);
|
||||||
|
terminal.loadAddon(ligaturesAddon);
|
||||||
|
```
|
||||||
|
|
||||||
|
### How It Works
|
||||||
|
|
||||||
|
In a browser environment, font ligature information is read directly by the web browser and used to render text correctly without any intervention from the developer. As of version 3, xterm.js uses the canvas to render characters individually, resulting in a significant performance boost. However, this means that it can no longer lean on the browser to determine when to draw font ligatures.
|
||||||
|
|
||||||
|
This package locates the font file on disk for the font currently in use by the terminal and parses the ligature information out of it (via the [font-ligatures] package). As text is rendered in xterm.js, this package annotates it with the locations of ligatures, allowing xterm.js to render it correctly.
|
||||||
|
|
||||||
|
Since this package depends on being able to find and resolve a system font from disk, it has to have system access that isn't available in the web browser. As a result, this package is mainly useful in environments that combine browser and Node.js runtimes (such as [Electron]).
|
||||||
|
|
||||||
|
### Fonts
|
||||||
|
|
||||||
|
This package makes use of the following fonts for testing:
|
||||||
|
|
||||||
|
* [Fira Code][Fira Code] - [Licensed under the OFL][Fira Code License] by Nikita
|
||||||
|
Prokopov, Mozilla Foundation with reserved names Fira Code, Fira Mono, and
|
||||||
|
Fira Sans
|
||||||
|
* [Iosevka] - [Licensed under the OFL][Iosevka License] by Belleve Invis with
|
||||||
|
reserved name Iosevka
|
||||||
|
|
||||||
|
[xterm.js]: https://github.com/xtermjs/xterm.js
|
||||||
|
[Electron]: https://electronjs.org/
|
||||||
|
[Node.js]: https://nodejs.org/
|
||||||
|
[font-ligatures]: https://github.com/princjef/font-ligatures
|
||||||
|
[Fira Code]: https://github.com/tonsky/FiraCode
|
||||||
|
[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
|
||||||
|
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const util = require('util');
|
||||||
|
|
||||||
|
const axios = require('axios').default;
|
||||||
|
const mkdirp = require('mkdirp');
|
||||||
|
const yauzl = require('yauzl');
|
||||||
|
|
||||||
|
const urls = {
|
||||||
|
fira: 'https://github.com/tonsky/FiraCode/raw/d42e7276fa925e5f82748f3ec9ea429736611b48/distr/otf/FiraCode-Regular.otf',
|
||||||
|
iosevka: 'https://github.com/be5invis/Iosevka/releases/download/v1.14.3/01-iosevka-1.14.3.zip'
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeFile = util.promisify(fs.writeFile);
|
||||||
|
const fontsFolder = path.join(__dirname, '../fonts');
|
||||||
|
|
||||||
|
async function download() {
|
||||||
|
await mkdirp(fontsFolder);
|
||||||
|
|
||||||
|
await downloadFiraCode();
|
||||||
|
await downloadIosevka();
|
||||||
|
|
||||||
|
console.log('Loaded all fonts for testing')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFiraCode() {
|
||||||
|
const file = path.join(fontsFolder, 'firaCode.otf');
|
||||||
|
if (await util.promisify(fs.exists)(file)) {
|
||||||
|
console.log('Fira Code already loaded');
|
||||||
|
} else {
|
||||||
|
console.log('Downloading Fira Code...');
|
||||||
|
await writeFile(
|
||||||
|
file,
|
||||||
|
(await axios.get(urls.fira, { responseType: 'arraybuffer' })).data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadIosevka() {
|
||||||
|
const file = path.join(fontsFolder, 'iosevka.ttf');
|
||||||
|
if (await util.promisify(fs.exists)(file)) {
|
||||||
|
console.log('Iosevka already loaded');
|
||||||
|
} else {
|
||||||
|
console.log('Downloading Iosevka...');
|
||||||
|
const iosevkaContents = (await axios.get(urls.iosevka, { responseType: 'arraybuffer' })).data;
|
||||||
|
const iosevkaZipfile = await util.promisify(yauzl.fromBuffer)(iosevkaContents);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
iosevkaZipfile.on('entry', entry => {
|
||||||
|
if (entry.fileName === 'ttf/iosevka-regular.ttf') {
|
||||||
|
iosevkaZipfile.openReadStream(entry, (err, stream) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
const writeStream = fs.createWriteStream(file);
|
||||||
|
stream.pipe(writeStream);
|
||||||
|
writeStream.on('close', () => resolve());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
download();
|
||||||
|
|
||||||
|
process.on('unhandledRejection', e => {
|
||||||
|
console.error(e);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
{
|
||||||
|
"name": "xterm-addon-ligatures",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"description": "Add support for programming ligatures to xterm.js",
|
||||||
|
"author": {
|
||||||
|
"name": "The xterm.js authors",
|
||||||
|
"url": "https://xtermjs.org/"
|
||||||
|
},
|
||||||
|
"main": "lib/xterm-addon-ligatures.js",
|
||||||
|
"types": "typings/xterm-addon-ligatures.d.ts",
|
||||||
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">8.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "node bin/download-fonts.js",
|
||||||
|
"build": "tsc -p src",
|
||||||
|
"watch": "tsc -w -p src",
|
||||||
|
"prepackage": "npm run build",
|
||||||
|
"package": "webpack",
|
||||||
|
"pretest": "npm run build",
|
||||||
|
"test": "nyc mocha out/**/*.test.js",
|
||||||
|
"prepublish": "npm run package"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"font",
|
||||||
|
"ligature",
|
||||||
|
"xterm",
|
||||||
|
"xterm.js",
|
||||||
|
"terminal"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"font-finder": "^1.1.0",
|
||||||
|
"font-ligatures": "^1.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/sinon": "^5.0.1",
|
||||||
|
"axios": "^0.18.0",
|
||||||
|
"mkdirp": "0.5.5",
|
||||||
|
"sinon": "6.3.5",
|
||||||
|
"yauzl": "^2.10.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { enableLigatures } from '.';
|
||||||
|
|
||||||
|
export interface ITerminalAddon {
|
||||||
|
activate(terminal: Terminal): void;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class LigaturesAddon implements ITerminalAddon {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
public activate(terminal: Terminal): void {
|
||||||
|
enableLigatures(terminal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { FontList } from 'font-finder';
|
||||||
|
import { Font, loadBuffer, loadFile } from 'font-ligatures';
|
||||||
|
|
||||||
|
import parse from './parse';
|
||||||
|
|
||||||
|
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
|
||||||
|
* resolved, throwing if it is unable to find a suitable match.
|
||||||
|
* @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<Font | undefined> {
|
||||||
|
if (!fontsPromise) {
|
||||||
|
// Web environment that supports font access API
|
||||||
|
if (typeof navigator !== 'undefined' && 'fonts' in navigator) {
|
||||||
|
try {
|
||||||
|
const status = await (navigator as IFontAccessNavigator).permissions.request?.({
|
||||||
|
name: 'local-fonts'
|
||||||
|
});
|
||||||
|
if (status && status.state !== 'granted') {
|
||||||
|
throw new Error('Permission to access local fonts not granted.');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// 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 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) {
|
||||||
|
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;
|
||||||
|
for (const family of parse(fontFamily)) {
|
||||||
|
// If we reach one of the generic font families, the font resolution
|
||||||
|
// will end for the browser and we can't determine the specific font
|
||||||
|
// used. Throw.
|
||||||
|
if (genericFontFamilies.includes(family)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fonts.hasOwnProperty(family) && fonts[family].length > 0) {
|
||||||
|
const font = fonts[family][0];
|
||||||
|
if ('blob' in font) {
|
||||||
|
return loadBuffer(await (await font.blob()).arrayBuffer(), { cacheSize });
|
||||||
|
}
|
||||||
|
return await loadFile(font.path, { cacheSize });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If none of the fonts could resolve, throw an error
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://drafts.csswg.org/css-fonts-4/#generic-font-families
|
||||||
|
const genericFontFamilies = [
|
||||||
|
'serif',
|
||||||
|
'sans-serif',
|
||||||
|
'cursive',
|
||||||
|
'fantasy',
|
||||||
|
'monospace',
|
||||||
|
'system-ui',
|
||||||
|
'emoji',
|
||||||
|
'math',
|
||||||
|
'fangsong'
|
||||||
|
];
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as sinon from 'sinon';
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import * as fontFinder from 'font-finder';
|
||||||
|
import * as fontLigatures from 'font-ligatures';
|
||||||
|
|
||||||
|
import * as ligatureSupport from '.';
|
||||||
|
|
||||||
|
describe('xterm-addon-ligatures', () => {
|
||||||
|
let onRefresh: sinon.SinonStub;
|
||||||
|
let term: MockTerminal;
|
||||||
|
|
||||||
|
// -> forms a ligature in Fira Code and Iosevka, but www only forms a ligature
|
||||||
|
// in Fira Code
|
||||||
|
const input = 'a -> b www c';
|
||||||
|
|
||||||
|
before(() => {
|
||||||
|
sinon.stub(fontFinder, 'list').returns(Promise.resolve({
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'Fira Code': [{
|
||||||
|
path: path.join(__dirname, '../fonts/firaCode.otf'),
|
||||||
|
style: fontFinder.Style.Regular,
|
||||||
|
type: fontFinder.Type.Monospace,
|
||||||
|
weight: 400
|
||||||
|
}],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'Iosevka': [{
|
||||||
|
path: path.join(__dirname, '../fonts/iosevka.ttf'),
|
||||||
|
style: fontFinder.Style.Regular,
|
||||||
|
type: fontFinder.Type.Monospace,
|
||||||
|
weight: 400
|
||||||
|
}],
|
||||||
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
'Nonexistant Font': [{
|
||||||
|
path: path.join(__dirname, '../fonts/nonexistant.ttf'),
|
||||||
|
style: fontFinder.Style.Regular,
|
||||||
|
type: fontFinder.Type.Monospace,
|
||||||
|
weight: 400
|
||||||
|
}]
|
||||||
|
} as fontFinder.FontList));
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
onRefresh = sinon.stub();
|
||||||
|
term = new MockTerminal(onRefresh);
|
||||||
|
ligatureSupport.enableLigatures(term as any);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers itself correctly', () => {
|
||||||
|
const term = new MockTerminal(sinon.spy());
|
||||||
|
assert.isUndefined(term.joiner);
|
||||||
|
ligatureSupport.enableLigatures(term as any);
|
||||||
|
assert.isFunction(term.joiner);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('registers itself correctly when called directly', () => {
|
||||||
|
const term = new MockTerminal(sinon.spy());
|
||||||
|
assert.isUndefined(term.joiner);
|
||||||
|
ligatureSupport.enableLigatures(term as any);
|
||||||
|
assert.isFunction(term.joiner);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an empty set of ranges on the first call while the font is loading', () => {
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the correct set of ranges once the font has loaded', done => {
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles quoted font names', done => {
|
||||||
|
term.setOption('fontFamily', '"Fira Code", monospace');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('falls back to later fonts if earlier ones are not present', done => {
|
||||||
|
term.setOption('fontFamily', 'notinstalled, Fira Code, monospace');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses the current font value', done => {
|
||||||
|
// The first three calls are all synchronous so that we don't allow time for
|
||||||
|
// any fonts to load while we're switching things around
|
||||||
|
term.setOption('fontFamily', 'Fira Code');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
term.setOption('fontFamily', 'notinstalled');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
term.setOption('fontFamily', 'Iosevka');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4]]);
|
||||||
|
|
||||||
|
// And switch it back to Fira Code for good measure
|
||||||
|
term.setOption('fontFamily', 'Fira Code');
|
||||||
|
|
||||||
|
// At this point, we haven't loaded the new font, so the result reverts
|
||||||
|
// back to empty until that happens
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows multiple terminal instances that use different fonts', done => {
|
||||||
|
const onRefresh2 = sinon.stub();
|
||||||
|
const term2 = new MockTerminal(onRefresh2);
|
||||||
|
term2.setOption('fontFamily', 'Iosevka');
|
||||||
|
ligatureSupport.enableLigatures(term2 as any);
|
||||||
|
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
onRefresh.callsFake(() => {
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
assert.deepEqual(term2.joiner!(input), []);
|
||||||
|
onRefresh2.callsFake(() => {
|
||||||
|
assert.deepEqual(term2.joiner!(input), [[2, 4]]);
|
||||||
|
assert.deepEqual(term.joiner!(input), [[2, 4], [7, 10]]);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if it finds but cannot load the font', async () => {
|
||||||
|
term.setOption('fontFamily', 'Nonexistant Font, monospace');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.throws(() => term.joiner!(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if the font is not present on the system', async () => {
|
||||||
|
term.setOption('fontFamily', 'notinstalled');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if no specific font is specified', async () => {
|
||||||
|
term.setOption('fontFamily', 'monospace');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns nothing if no fonts are provided', async () => {
|
||||||
|
term.setOption('fontFamily', '');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails when given malformed inputs', async () => {
|
||||||
|
term.setOption('fontFamily', {} as any);
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.throws(() => term.joiner!(input));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ensures no empty errors are thrown', async () => {
|
||||||
|
sinon.stub(fontLigatures, 'loadFile').callsFake(async () => { throw undefined; });
|
||||||
|
term.setOption('fontFamily', 'Iosevka');
|
||||||
|
assert.deepEqual(term.joiner!(input), []);
|
||||||
|
await delay(500);
|
||||||
|
assert.isTrue(onRefresh.notCalled);
|
||||||
|
assert.throws(() => term.joiner!(input), 'Failure while loading font');
|
||||||
|
(fontLigatures.loadFile as sinon.SinonStub).restore();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
class MockTerminal {
|
||||||
|
private _options: { [name: string]: string | number } = {
|
||||||
|
fontFamily: 'Fira Code, monospace',
|
||||||
|
rows: 50
|
||||||
|
};
|
||||||
|
public joiner?: (text: string) => [number, number][];
|
||||||
|
public refresh: (start: number, end: number) => void;
|
||||||
|
|
||||||
|
constructor(onRefresh: (start: number, end: number) => void) {
|
||||||
|
this.refresh = onRefresh;
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||||
|
this.joiner = handler;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
public deregisterCharacterJoiner(id: number): void {
|
||||||
|
this.joiner = undefined;
|
||||||
|
}
|
||||||
|
public setOption(name: string, value: string | number): void {
|
||||||
|
this._options[name] = value;
|
||||||
|
}
|
||||||
|
public getOption(name: string): string | number {
|
||||||
|
return this._options[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function delay(delayMs: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, delayMs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { Font } from 'font-ligatures';
|
||||||
|
|
||||||
|
import load from './font';
|
||||||
|
|
||||||
|
const enum LoadingState {
|
||||||
|
UNLOADED,
|
||||||
|
LOADING,
|
||||||
|
LOADED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
// Caches 100K characters worth of ligatures. In practice this works out to
|
||||||
|
// about 650 KB worth of cache, when a moderate number of ligatures are present.
|
||||||
|
const CACHE_SIZE = 100000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable ligature support for the provided Terminal instance. To function
|
||||||
|
* properly, this must be called after `open()` is called on the therminal. If
|
||||||
|
* the font currently in use supports ligatures, the terminal will automatically
|
||||||
|
* start to render them.
|
||||||
|
* @param term Terminal instance from xterm.js
|
||||||
|
*/
|
||||||
|
export function enableLigatures(term: Terminal): void {
|
||||||
|
let currentFontName: string | undefined = undefined;
|
||||||
|
let font: Font | undefined = undefined;
|
||||||
|
let loadingState: LoadingState = LoadingState.UNLOADED;
|
||||||
|
let loadError: any | undefined = undefined;
|
||||||
|
|
||||||
|
term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||||
|
// If the font hasn't been loaded yet, load it and return an empty result
|
||||||
|
const termFont = term.getOption('fontFamily');
|
||||||
|
if (
|
||||||
|
termFont &&
|
||||||
|
(loadingState === LoadingState.UNLOADED || currentFontName !== termFont)
|
||||||
|
) {
|
||||||
|
font = undefined;
|
||||||
|
loadingState = LoadingState.LOADING;
|
||||||
|
currentFontName = termFont;
|
||||||
|
const currentCallFontName = currentFontName;
|
||||||
|
|
||||||
|
load(currentCallFontName, CACHE_SIZE)
|
||||||
|
.then(f => {
|
||||||
|
// Another request may have come in while we were waiting, so make
|
||||||
|
// sure our font is still vaild.
|
||||||
|
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||||
|
loadingState = LoadingState.LOADED;
|
||||||
|
font = f;
|
||||||
|
|
||||||
|
// Only refresh things if we actually found a font
|
||||||
|
if (f) {
|
||||||
|
term.refresh(0, term.getOption('rows') - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
// Another request may have come in while we were waiting, so make
|
||||||
|
// sure our font is still vaild.
|
||||||
|
if (currentCallFontName === term.getOption('fontFamily')) {
|
||||||
|
loadingState = LoadingState.FAILED;
|
||||||
|
font = undefined;
|
||||||
|
loadError = e;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font && loadingState === LoadingState.LOADED) {
|
||||||
|
// We clone the entries to avoid the internal cache of the ligature finder
|
||||||
|
// getting messed up.
|
||||||
|
return font.findLigatureRanges(text).map<[number, number]>(
|
||||||
|
range => [range[0], range[1]]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (loadingState === LoadingState.FAILED) {
|
||||||
|
throw loadError || new Error('Failure while loading font');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
|
||||||
|
import parse from './parse';
|
||||||
|
|
||||||
|
// TODO: integrate tests from http://test.csswg.org/suites/css-fonts-4_dev/nightly-unstable/
|
||||||
|
describe('parse', () => {
|
||||||
|
it('parses individual families', () => {
|
||||||
|
assert.deepEqual(parse('monospace'), ['monospace']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses multiple families', () => {
|
||||||
|
assert.deepEqual(parse('Arial, Verdana, serif'), ['Arial', 'Verdana', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses quoted families', () => {
|
||||||
|
assert.deepEqual(parse('"Times New Roman", serif'), ['Times New Roman', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses single quoted families', () => {
|
||||||
|
assert.deepEqual(parse('\'Times New Roman\', serif'), ['Times New Roman', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses families with spaces in their names', () => {
|
||||||
|
assert.deepEqual(parse('Times New Roman, serif'), ['Times New Roman', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('collapses multiple spaces together in identifiers', () => {
|
||||||
|
assert.deepEqual(parse('Times New Roman, serif'), ['Times New Roman', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not collapse multiple spaces together in quoted strings', () => {
|
||||||
|
assert.deepEqual(parse('"Times New Roman", serif'), ['Times New Roman', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles escaped characters in strings', () => {
|
||||||
|
assert.deepEqual(parse('"quote \\" slash \\\\ slashquote \\\\\\"", serif'), ['quote " slash \\ slashquote \\"', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if a family has an unterminated string', () => {
|
||||||
|
assert.throws(() => parse('"Unterminated, serif'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unicode escape sequences', () => {
|
||||||
|
assert.deepEqual(parse('"space\\20 between", serif'), ['space between', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swallows only the first space after a unicode escape', () => {
|
||||||
|
assert.deepEqual(parse('"two-space\\20 between", serif'), ['two-space between', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('automatically ends the unicode escape after six digits', () => {
|
||||||
|
assert.deepEqual(parse('space\\000020between, serif'), ['space between', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unicode escapes at the end of the family', () => {
|
||||||
|
assert.deepEqual(parse('endswithbrace \\7b, serif'), ['endswithbrace {', 'serif']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles unicode escapes at the end of the input', () => {
|
||||||
|
assert.deepEqual(parse('endswithbrace \\7b'), ['endswithbrace {']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles other escaped characters in identifiers', () => {
|
||||||
|
assert.deepEqual(parse('has\\,comma'), ['has,comma']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swallows escaped newlines in strings', () => {
|
||||||
|
assert.deepEqual(parse('"multi \\\nline", serif'), ['multi line', 'serif']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface IParseContext {
|
||||||
|
input: string;
|
||||||
|
offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses a CSS font family value, returning the component font families
|
||||||
|
* contained within.
|
||||||
|
*
|
||||||
|
* @param family The CSS font family input string to parse
|
||||||
|
*/
|
||||||
|
export default function parse(family: string): string[] {
|
||||||
|
if (typeof family !== 'string') {
|
||||||
|
throw new Error('Font family must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: IParseContext = {
|
||||||
|
input: family,
|
||||||
|
offset: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
const families = [];
|
||||||
|
let currentFamily = '';
|
||||||
|
|
||||||
|
// Work through the input character by character until there are none left.
|
||||||
|
// This lexing and parsing in one pass.
|
||||||
|
while (context.offset < context.input.length) {
|
||||||
|
const char = context.input[context.offset++];
|
||||||
|
switch (char) {
|
||||||
|
// String
|
||||||
|
case '\'':
|
||||||
|
case '"':
|
||||||
|
currentFamily += parseString(context, char);
|
||||||
|
break;
|
||||||
|
// End of family
|
||||||
|
case ',':
|
||||||
|
families.push(currentFamily);
|
||||||
|
currentFamily = '';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Identifiers (whitespace between families is swallowed)
|
||||||
|
if (!/\s/.test(char)) {
|
||||||
|
context.offset--;
|
||||||
|
currentFamily += parseIdentifier(context);
|
||||||
|
families.push(currentFamily);
|
||||||
|
currentFamily = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return families;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSS string.
|
||||||
|
*
|
||||||
|
* @param context Parsing input and offset
|
||||||
|
* @param quoteChar The quote character for the string (' or ")
|
||||||
|
*/
|
||||||
|
function parseString(context: IParseContext, quoteChar: '\'' | '"'): string {
|
||||||
|
let str = '';
|
||||||
|
let escaped = false;
|
||||||
|
while (context.offset < context.input.length) {
|
||||||
|
const char = context.input[context.offset++];
|
||||||
|
if (escaped) {
|
||||||
|
if (/[\dA-Fa-f]/.test(char)) {
|
||||||
|
// Unicode escape
|
||||||
|
context.offset--;
|
||||||
|
str += parseUnicode(context);
|
||||||
|
} else if (char !== '\n') {
|
||||||
|
// Newlines are ignored if escaped. Other characters are used as is.
|
||||||
|
str += char;
|
||||||
|
}
|
||||||
|
escaped = false;
|
||||||
|
} else {
|
||||||
|
switch (char) {
|
||||||
|
// Terminated quote
|
||||||
|
case quoteChar:
|
||||||
|
return str;
|
||||||
|
// Begin escape
|
||||||
|
case '\\':
|
||||||
|
escaped = true;
|
||||||
|
break;
|
||||||
|
// Add character to string
|
||||||
|
default:
|
||||||
|
str += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Unterminated string');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSS custom identifier.
|
||||||
|
*
|
||||||
|
* @param context Parsing input and offset
|
||||||
|
*/
|
||||||
|
function parseIdentifier(context: IParseContext): string {
|
||||||
|
let str = '';
|
||||||
|
let escaped = false;
|
||||||
|
while (context.offset < context.input.length) {
|
||||||
|
const char = context.input[context.offset++];
|
||||||
|
if (escaped) {
|
||||||
|
if (/[\dA-Fa-f]/.test(char)) {
|
||||||
|
// Unicode escape
|
||||||
|
context.offset--;
|
||||||
|
str += parseUnicode(context);
|
||||||
|
} else {
|
||||||
|
// Everything else is used as is
|
||||||
|
str += char;
|
||||||
|
}
|
||||||
|
escaped = false;
|
||||||
|
} else {
|
||||||
|
switch (char) {
|
||||||
|
// Begin escape
|
||||||
|
case '\\':
|
||||||
|
escaped = true;
|
||||||
|
break;
|
||||||
|
// Terminate identifier
|
||||||
|
case ',':
|
||||||
|
return str;
|
||||||
|
default:
|
||||||
|
if (/\s/.test(char)) {
|
||||||
|
// Whitespace is collapsed into a single space within an identifier
|
||||||
|
if (!str.endsWith(' ')) {
|
||||||
|
str += ' ';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Add other characters directly
|
||||||
|
str += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a CSS unicode escape.
|
||||||
|
*
|
||||||
|
* @param context Parsing input and offset
|
||||||
|
*/
|
||||||
|
function parseUnicode(context: IParseContext): string {
|
||||||
|
let str = '';
|
||||||
|
while (context.offset < context.input.length) {
|
||||||
|
const char = context.input[context.offset++];
|
||||||
|
if (/\s/.test(char)) {
|
||||||
|
// The first whitespace character after a unicode escape indicates the end
|
||||||
|
// of the escape and is swallowed.
|
||||||
|
return unicodeToString(str);
|
||||||
|
}
|
||||||
|
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--;
|
||||||
|
return unicodeToString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, just add it to the escape
|
||||||
|
str += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
return unicodeToString(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a unicode code point from a hex string to a utf8 string.
|
||||||
|
*
|
||||||
|
* @param codePoint Unicode code point represented as a hex string
|
||||||
|
*/
|
||||||
|
function unicodeToString(codePoint: string): string {
|
||||||
|
return String.fromCodePoint(parseInt(codePoint, 16));
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "commonjs",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "../out",
|
||||||
|
"rootDir": ".",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"preserveWatchOutput": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* This contains the type declarations for the xterm-addon-ligatures library.
|
||||||
|
* Note that some interfaces may differ between this file and the actual
|
||||||
|
* implementation in src/, that's because this file declares the *public* API
|
||||||
|
* which is intended to be stable and consumed by external programs.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm';
|
||||||
|
|
||||||
|
declare module 'xterm-addon-ligatures' {
|
||||||
|
/**
|
||||||
|
* An xterm.js addon that enables web links.
|
||||||
|
*/
|
||||||
|
export class LigaturesAddon implements ITerminalAddon {
|
||||||
|
/**
|
||||||
|
* Creates a new ligatures addon.
|
||||||
|
*/
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates the addon
|
||||||
|
* @param terminal The terminal the addon is being loaded in.
|
||||||
|
*/
|
||||||
|
public activate(terminal: Terminal): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the addon.
|
||||||
|
*/
|
||||||
|
public dispose(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const addonName = 'LigaturesAddon';
|
||||||
|
const mainFile = 'xterm-addon-ligatures.js';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: `./out/${addonName}.js`,
|
||||||
|
devtool: 'source-map',
|
||||||
|
target: 'electron-renderer',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre",
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: mainFile,
|
||||||
|
path: path.resolve('./lib'),
|
||||||
|
library: addonName,
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
mode: 'production',
|
||||||
|
externals: {
|
||||||
|
'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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||||
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"@sinonjs/commons@^1", "@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0":
|
||||||
|
version "1.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d"
|
||||||
|
integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q==
|
||||||
|
dependencies:
|
||||||
|
type-detect "4.0.8"
|
||||||
|
|
||||||
|
"@sinonjs/formatio@^3.0.0", "@sinonjs/formatio@^3.2.1":
|
||||||
|
version "3.2.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c"
|
||||||
|
integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1"
|
||||||
|
"@sinonjs/samsam" "^3.1.0"
|
||||||
|
|
||||||
|
"@sinonjs/samsam@^2.1.2":
|
||||||
|
version "2.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-2.1.3.tgz#62cf2a9b624edc795134135fe37fc2ae8ea36be3"
|
||||||
|
integrity sha512-8zNeBkSKhU9a5cRNbpCKau2WWPfan+Q2zDlcXvXyhn9EsMqgYs4qzo0XHNVlXC6ABQL8fT6nV+zzo5RTHJzyXw==
|
||||||
|
|
||||||
|
"@sinonjs/samsam@^3.1.0":
|
||||||
|
version "3.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a"
|
||||||
|
integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.3.0"
|
||||||
|
array-from "^2.1.1"
|
||||||
|
lodash "^4.17.15"
|
||||||
|
|
||||||
|
"@sinonjs/text-encoding@^0.7.1":
|
||||||
|
version "0.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5"
|
||||||
|
integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==
|
||||||
|
|
||||||
|
"@types/sinon@^5.0.1":
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-5.0.1.tgz#a15b36ec42f1f53166617491feabd1734cb03e21"
|
||||||
|
|
||||||
|
array-from@^2.1.1:
|
||||||
|
version "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"
|
||||||
|
dependencies:
|
||||||
|
follow-redirects "1.5.10"
|
||||||
|
is-buffer "^2.0.2"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
fd-slicer@~1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
|
||||||
|
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"
|
||||||
|
|
||||||
|
font-finder@^1.0.3:
|
||||||
|
version "1.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/font-finder/-/font-finder-1.0.4.tgz#2ca944954dd8d0e1b5bdc4c596cc08607761d89b"
|
||||||
|
dependencies:
|
||||||
|
get-system-fonts "^2.0.0"
|
||||||
|
promise-stream-reader "^1.0.1"
|
||||||
|
|
||||||
|
font-finder@^1.1.0:
|
||||||
|
version "1.1.0"
|
||||||
|
resolved "https://registry.npmjs.org/font-finder/-/font-finder-1.1.0.tgz#2bff2b2762acba720239c8bec898a96daae90858"
|
||||||
|
integrity sha512-wpCL2uIbi6GurJbU7ZlQ3nGd61Ho+dSU6U83/xJT5UPFfN35EeCW/rOtS+5k+IuEZu2SYmHzDIPL9eA5tSYRAw==
|
||||||
|
dependencies:
|
||||||
|
get-system-fonts "^2.0.0"
|
||||||
|
promise-stream-reader "^1.0.1"
|
||||||
|
|
||||||
|
font-ligatures@^1.4.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 "^6.0.0"
|
||||||
|
opentype.js "^0.8.0"
|
||||||
|
|
||||||
|
get-system-fonts@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
just-extend@^4.0.2:
|
||||||
|
version "4.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4"
|
||||||
|
integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA==
|
||||||
|
|
||||||
|
lodash.get@^4.4.2:
|
||||||
|
version "4.4.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||||
|
|
||||||
|
lodash@^4.17.15:
|
||||||
|
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"
|
||||||
|
resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733"
|
||||||
|
integrity sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==
|
||||||
|
|
||||||
|
lolex@^5.0.1:
|
||||||
|
version "5.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367"
|
||||||
|
integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.7.0"
|
||||||
|
|
||||||
|
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:
|
||||||
|
yallist "^4.0.0"
|
||||||
|
|
||||||
|
minimist@^1.2.5:
|
||||||
|
version "1.2.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
|
||||||
|
integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
|
||||||
|
|
||||||
|
mkdirp@0.5.5:
|
||||||
|
version "0.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def"
|
||||||
|
integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==
|
||||||
|
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"
|
||||||
|
integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/formatio" "^3.2.1"
|
||||||
|
"@sinonjs/text-encoding" "^0.7.1"
|
||||||
|
just-extend "^4.0.2"
|
||||||
|
lolex "^5.0.1"
|
||||||
|
path-to-regexp "^1.7.0"
|
||||||
|
|
||||||
|
opentype.js@^0.8.0:
|
||||||
|
version "0.8.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/opentype.js/-/opentype.js-0.8.0.tgz#acabcfa1642fbe894a3e4d759e43ba694e02bd35"
|
||||||
|
dependencies:
|
||||||
|
tiny-inflate "^1.0.2"
|
||||||
|
|
||||||
|
path-to-regexp@^1.7.0:
|
||||||
|
version "1.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
|
||||||
|
dependencies:
|
||||||
|
isarray "0.0.1"
|
||||||
|
|
||||||
|
pend@~1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
sinon@6.3.5:
|
||||||
|
version "6.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0"
|
||||||
|
integrity sha512-xgoZ2gKjyVRcF08RrIQc+srnSyY1JDJtxu3Nsz07j1ffjgXoY6uPLf/qja6nDBZgzYYEovVkFryw2+KiZz11xQ==
|
||||||
|
dependencies:
|
||||||
|
"@sinonjs/commons" "^1.0.2"
|
||||||
|
"@sinonjs/formatio" "^3.0.0"
|
||||||
|
"@sinonjs/samsam" "^2.1.2"
|
||||||
|
diff "^3.5.0"
|
||||||
|
lodash.get "^4.4.2"
|
||||||
|
lolex "^2.7.5"
|
||||||
|
nise "^1.4.5"
|
||||||
|
supports-color "^5.5.0"
|
||||||
|
type-detect "^4.0.8"
|
||||||
|
|
||||||
|
supports-color@^5.5.0:
|
||||||
|
version "5.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f"
|
||||||
|
integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==
|
||||||
|
dependencies:
|
||||||
|
has-flag "^3.0.0"
|
||||||
|
|
||||||
|
tiny-inflate@^1.0.2:
|
||||||
|
version "1.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/tiny-inflate/-/tiny-inflate-1.0.2.tgz#93d9decffc8805bd57eae4310f0b745e9b6fb3a7"
|
||||||
|
|
||||||
|
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@^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"
|
||||||
|
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
|
||||||
|
dependencies:
|
||||||
|
buffer-crc32 "~0.2.3"
|
||||||
|
fd-slicer "~1.1.0"
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-search",
|
"name": "xterm-addon-search",
|
||||||
"version": "0.4.0",
|
"version": "0.8.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../../node_modules/.bin/tsc -p src",
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
// Search from startRow + 1 to end
|
// Search from startRow + 1 to end
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
||||||
for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) {
|
for (let y = startRow + 1; y < this._terminal.buffer.active.baseY + this._terminal.rows; y++) {
|
||||||
searchPosition.startRow = y;
|
searchPosition.startRow = y;
|
||||||
searchPosition.startCol = 0;
|
searchPosition.startCol = 0;
|
||||||
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
// If the current line is wrapped line, increase index of column to ignore the previous scan
|
||||||
@@ -110,8 +110,12 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is only one result, return true.
|
// If there is only one result, wrap back and return selection if it exists.
|
||||||
if (!result && currentSelection) return true;
|
if (!result && currentSelection) {
|
||||||
|
searchPosition.startRow = currentSelection.startRow;
|
||||||
|
searchPosition.startCol = 0;
|
||||||
|
result = this._findInLine(term, searchPosition, searchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
// Set selection and scroll if a result was found
|
// Set selection and scroll if a result was found
|
||||||
return this._selectResult(result);
|
return this._selectResult(result);
|
||||||
@@ -135,7 +139,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isReverseSearch = true;
|
const isReverseSearch = true;
|
||||||
let startRow = this._terminal.buffer.baseY + this._terminal.rows;
|
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
|
||||||
let startCol = this._terminal.cols;
|
let startCol = this._terminal.cols;
|
||||||
let result: ISearchResult | undefined;
|
let result: ISearchResult | undefined;
|
||||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||||
@@ -154,8 +158,15 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (incremental) {
|
if (incremental) {
|
||||||
|
// Try to expand selection to right first.
|
||||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||||
if (!(result && result.row === startRow && result.col === startCol)) {
|
const isOldResultHighlighted = result && result.row === startRow && result.col === startCol;
|
||||||
|
if (!isOldResultHighlighted) {
|
||||||
|
// If selection was not able to be expanded to the right, then try reverse search
|
||||||
|
if (currentSelection) {
|
||||||
|
searchPosition.startRow = currentSelection.endRow;
|
||||||
|
searchPosition.startCol = currentSelection.endColumn;
|
||||||
|
}
|
||||||
result = this._findInLine(term, searchPosition, searchOptions, true);
|
result = this._findInLine(term, searchPosition, searchOptions, true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -174,8 +185,8 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we hit the top and didn't search from the very bottom wrap back down
|
// If we hit the top and didn't search from the very bottom wrap back down
|
||||||
if (!result && startRow !== (this._terminal.buffer.baseY + this._terminal.rows)) {
|
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
|
||||||
for (let y = (this._terminal.buffer.baseY + this._terminal.rows); y > startRow; y--) {
|
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
|
||||||
searchPosition.startRow = y;
|
searchPosition.startRow = y;
|
||||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -197,7 +208,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
private _initLinesCache(): void {
|
private _initLinesCache(): void {
|
||||||
const terminal = this._terminal!;
|
const terminal = this._terminal!;
|
||||||
if (!this._linesCache) {
|
if (!this._linesCache) {
|
||||||
this._linesCache = new Array(terminal.buffer.length);
|
this._linesCache = new Array(terminal.buffer.active.length);
|
||||||
this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache());
|
this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache());
|
||||||
this._resizeListener = terminal.onResize(() => this._destroyLinesCache());
|
this._resizeListener = terminal.onResize(() => this._destroyLinesCache());
|
||||||
}
|
}
|
||||||
@@ -229,8 +240,8 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
* @param term the substring that starts at searchIndex
|
* @param term the substring that starts at searchIndex
|
||||||
*/
|
*/
|
||||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||||
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
|
return ((searchIndex === 0) || (NON_WORD_CHARACTERS.includes(line[searchIndex - 1]))) &&
|
||||||
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
|
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.includes(line[searchIndex + term.length])));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -241,6 +252,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
* @param term The search term.
|
* @param term The search term.
|
||||||
* @param position The position to start the search.
|
* @param position The position to start the search.
|
||||||
* @param searchOptions Search options.
|
* @param searchOptions Search options.
|
||||||
|
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
|
||||||
* @return The search result if it was found.
|
* @return The search result if it was found.
|
||||||
*/
|
*/
|
||||||
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
||||||
@@ -249,8 +261,8 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
const col = searchPosition.startCol;
|
const col = searchPosition.startCol;
|
||||||
|
|
||||||
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
// Ignore wrapped lines, only consider on unwrapped line (first row of command string).
|
||||||
const firstLine = terminal.buffer.getLine(row);
|
const firstLine = terminal.buffer.active.getLine(row);
|
||||||
if (firstLine && firstLine.isWrapped) {
|
if (firstLine?.isWrapped) {
|
||||||
if (isReverseSearch) {
|
if (isReverseSearch) {
|
||||||
searchPosition.startCol += terminal.cols;
|
searchPosition.startCol += terminal.cols;
|
||||||
return;
|
return;
|
||||||
@@ -311,7 +323,7 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const line = terminal.buffer.getLine(row);
|
const line = terminal.buffer.active.getLine(row);
|
||||||
|
|
||||||
if (line) {
|
if (line) {
|
||||||
for (let i = 0; i < resultIndex; i++) {
|
for (let i = 0; i < resultIndex; i++) {
|
||||||
@@ -320,13 +332,13 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Adjust the searchIndex to normalize emoji into single chars
|
// Adjust the searchIndex to normalize emoji into single chars
|
||||||
const char = cell.char;
|
const char = cell.getChars();
|
||||||
if (char.length > 1) {
|
if (char.length > 1) {
|
||||||
resultIndex -= char.length - 1;
|
resultIndex -= char.length - 1;
|
||||||
}
|
}
|
||||||
// Adjust the searchIndex for empty characters following wide unicode
|
// Adjust the searchIndex for empty characters following wide unicode
|
||||||
// chars (eg. CJK)
|
// chars (eg. CJK)
|
||||||
const charWidth = cell.width;
|
const charWidth = cell.getWidth();
|
||||||
if (charWidth === 0) {
|
if (charWidth === 0) {
|
||||||
resultIndex++;
|
resultIndex++;
|
||||||
}
|
}
|
||||||
@@ -354,9 +366,9 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
let lineWrapsToNext: boolean;
|
let lineWrapsToNext: boolean;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
const nextLine = terminal.buffer.getLine(lineIndex + 1);
|
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
|
||||||
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||||
const line = terminal.buffer.getLine(lineIndex);
|
const line = terminal.buffer.active.getLine(lineIndex);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -380,9 +392,9 @@ export class SearchAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
terminal.select(result.col, result.row, result.term.length);
|
terminal.select(result.col, result.row, result.term.length);
|
||||||
// If it is not in the viewport then we scroll else it just gets selected
|
// If it is not in the viewport then we scroll else it just gets selected
|
||||||
if (result.row >= (terminal.buffer.viewportY + terminal.rows) || result.row < terminal.buffer.viewportY) {
|
if (result.row >= (terminal.buffer.active.viewportY + terminal.rows) || result.row < terminal.buffer.active.viewportY) {
|
||||||
let scroll = result.row - terminal.buffer.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);
|
terminal.scrollLines(scroll);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"outDir": "../out",
|
"outDir": "../out",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
|
|||||||
+38
-47
@@ -3,31 +3,29 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as puppeteer from 'puppeteer';
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { ITerminalOptions } from 'xterm';
|
|
||||||
import { readFile } from 'fs';
|
import { readFile } from 'fs';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
import { openTerminal, writeSync, getBrowserType } 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: puppeteer.Browser;
|
let browser: Browser;
|
||||||
let page: puppeteer.Page;
|
let page: Page;
|
||||||
const width = 800;
|
const width = 800;
|
||||||
const height = 600;
|
const height = 600;
|
||||||
|
|
||||||
describe('Search Tests', function (): void {
|
describe('Search Tests', function(): void {
|
||||||
this.timeout(20000);
|
before(async function(): Promise<any> {
|
||||||
|
const browserType = getBrowserType();
|
||||||
before(async function (): Promise<any> {
|
browser = await browserType.launch({
|
||||||
browser = await puppeteer.launch({
|
headless: process.argv.includes('--headless')
|
||||||
headless: process.argv.indexOf('--headless') !== -1,
|
|
||||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
|
||||||
});
|
});
|
||||||
page = (await browser.pages())[0];
|
page = await (await browser.newContext()).newPage();
|
||||||
await page.setViewport({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
await page.goto(APP);
|
await page.goto(APP);
|
||||||
await openTerminal();
|
await openTerminal(page);
|
||||||
await page.evaluate(`window.search = new SearchAddon();`);
|
await page.evaluate(`window.search = new SearchAddon();`);
|
||||||
await page.evaluate(`window.term.loadAddon(window.search);`);
|
await page.evaluate(`window.term.loadAddon(window.search);`);
|
||||||
});
|
});
|
||||||
@@ -41,7 +39,7 @@ describe('Search Tests', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Simple Search', async () => {
|
it('Simple Search', async () => {
|
||||||
await writeSync('dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
|
await writeSync(page, 'dafhdjfldshafhldsahfkjhldhjkftestlhfdsakjfhdjhlfdsjkafhjdlk');
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findNext('test')`), true);
|
||||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
|
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'test');
|
||||||
});
|
});
|
||||||
@@ -54,16 +52,16 @@ describe('Search Tests', function (): void {
|
|||||||
}
|
}
|
||||||
dataString += makeData(50);
|
dataString += makeData(50);
|
||||||
}
|
}
|
||||||
await writeSync(dataString);
|
await writeSync(page, dataString);
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findNext('$^1_3{}test$#')`), true);
|
||||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
|
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '$^1_3{}test$#');
|
||||||
});
|
});
|
||||||
it ('Incremental Find Previous', async () => {
|
it('Incremental Find Previous', async () => {
|
||||||
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
||||||
await writeSync('package.json pack package.lock');
|
await writeSync(page, 'package.json pack package.lock');
|
||||||
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
||||||
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||||
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
|
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||||
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
||||||
@@ -71,16 +69,16 @@ describe('Search Tests', function (): void {
|
|||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||||
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
||||||
// We have to reevaluate line because it should have switched starting rows at this point
|
// We have to reevaluate line because it should have switched starting rows at this point
|
||||||
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||||
});
|
});
|
||||||
it ('Incremental Find Next', async () => {
|
it('Incremental Find Next', async () => {
|
||||||
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
||||||
await writeSync('package.jsonc');
|
await writeSync(page, 'package.jsonc');
|
||||||
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
||||||
let line: string = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||||
let selectionPosition: {startColumn: number, startRow: number, endColumn: number, endRow: number} = await page.evaluate(`window.term.getSelectionPosition()`);
|
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||||
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
||||||
@@ -88,12 +86,12 @@ describe('Search Tests', function (): void {
|
|||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||||
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
||||||
// We have to reevaluate line because it should have switched starting rows at this point
|
// We have to reevaluate line because it should have switched starting rows at this point
|
||||||
line = await page.evaluate(`window.term.buffer.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||||
});
|
});
|
||||||
it('Simple Regex', async () => {
|
it('Simple Regex', async () => {
|
||||||
await writeSync('abc123defABCD');
|
await writeSync(page, 'abc123defABCD');
|
||||||
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
|
await page.evaluate(`window.search.findNext('[a-z]+', {regex: true})`);
|
||||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||||
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
|
await page.evaluate(`window.search.findNext('[A-Z]+', {regex: true, caseSensitive: true})`);
|
||||||
@@ -101,7 +99,7 @@ describe('Search Tests', function (): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Search for single result twice should not unselect it', async () => {
|
it('Search for single result twice should not unselect it', async () => {
|
||||||
await writeSync('abc def');
|
await writeSync(page, 'abc def');
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), 'abc');
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findNext('abc')`), true);
|
||||||
@@ -113,12 +111,19 @@ describe('Search Tests', function (): void {
|
|||||||
let fixture: string;
|
let fixture: string;
|
||||||
before(async () => {
|
before(async () => {
|
||||||
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
|
const rawFixture = await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)));
|
||||||
fixture = rawFixture.toString()
|
if (process.platform === 'win32') {
|
||||||
.replace(/\n/g, '\\n\\r')
|
fixture = rawFixture.toString()
|
||||||
|
.replace(/\n/g, '\\n')
|
||||||
|
.replace(/\r/g, '\\r');
|
||||||
|
} else {
|
||||||
|
fixture = rawFixture.toString()
|
||||||
|
.replace(/\n/g, '\\n\\r');
|
||||||
|
}
|
||||||
|
fixture = fixture
|
||||||
.replace(/'/g, '\\\'');
|
.replace(/'/g, '\\\'');
|
||||||
});
|
});
|
||||||
it('should find all occurrences using findNext', async () => {
|
it('should find all occurrences using findNext', async () => {
|
||||||
await writeSync(fixture);
|
await writeSync(page, fixture);
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||||
@@ -152,7 +157,7 @@ describe('Search Tests', function (): void {
|
|||||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||||
});
|
});
|
||||||
it('should find all occurrences using findPrevious', async () => {
|
it('should find all occurrences using findPrevious', async () => {
|
||||||
await writeSync(fixture);
|
await writeSync(page, fixture);
|
||||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||||
@@ -189,20 +194,6 @@ describe('Search Tests', function (): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
|
||||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
|
||||||
if (options.rendererType === 'dom') {
|
|
||||||
await page.waitForSelector('.xterm-rows');
|
|
||||||
} else {
|
|
||||||
await page.waitForSelector('.xterm-text-layer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeSync(data: string): Promise<void> {
|
|
||||||
return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeData(length: number): string {
|
function makeData(length: number): string {
|
||||||
let result = '';
|
let result = '';
|
||||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es6",
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
lib
|
||||||
|
node_modules
|
||||||
|
out-benchmark
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
**/*.api.js
|
||||||
|
**/*.api.ts
|
||||||
|
tsconfig.json
|
||||||
|
.yarnrc
|
||||||
|
webpack.config.js
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
## xterm-addon-serialize
|
||||||
|
|
||||||
|
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables xterm.js to serialize a terminal framebuffer into string or html. This addon requires xterm.js v4+.
|
||||||
|
|
||||||
|
⚠️ This is an experimental addon that is still under construction ⚠️
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save xterm-addon-serialize
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Terminal } from "xterm";
|
||||||
|
import { SerializeAddon } from "xterm-addon-serialize";
|
||||||
|
|
||||||
|
const terminal = new Terminal();
|
||||||
|
const serializeAddon = new SerializeAddon();
|
||||||
|
terminal.loadAddon(serializeAddon);
|
||||||
|
|
||||||
|
terminal.write("something...", () => {
|
||||||
|
console.log(serializeAddon.serialize());
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-serialize/typings/xterm-addon-serialize.d.ts) for more advanced usage.
|
||||||
|
|
||||||
|
### Benchmark
|
||||||
|
|
||||||
|
⚠️ Ensure you have `lolcat`, `hexdump` programs installed in your computer
|
||||||
|
|
||||||
|
```shell
|
||||||
|
$ git clone https://github.com/xtermjs/xterm.js.git
|
||||||
|
$ cd xterm.js
|
||||||
|
$ yarn
|
||||||
|
$ cd addons/xterm-addon-serialize
|
||||||
|
$ yarn benchmark && yarn benchmark-baseline
|
||||||
|
$ # change some code in `xterm-addon-serialize`
|
||||||
|
$ yarn benchmark-eval
|
||||||
|
```
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
||||||
|
|
||||||
|
import { spawn } from 'node-pty';
|
||||||
|
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||||
|
import { Terminal } from 'browser/public/Terminal';
|
||||||
|
import { SerializeAddon } from 'SerializeAddon';
|
||||||
|
|
||||||
|
class TestTerminal extends Terminal {
|
||||||
|
public writeSync(data: string): void {
|
||||||
|
(this as any)._core.writeSync(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
perfContext('Terminal: sh -c "dd if=/dev/urandom count=40 bs=1k | hexdump | lolcat -f"', () => {
|
||||||
|
let content = '';
|
||||||
|
let contentUtf8: Uint8Array;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
const p = spawn('sh', ['-c', 'dd if=/dev/urandom count=40 bs=1k | hexdump | lolcat -f'], {
|
||||||
|
name: 'xterm-256color',
|
||||||
|
cols: 80,
|
||||||
|
rows: 25,
|
||||||
|
cwd: process.env.HOME,
|
||||||
|
env: process.env,
|
||||||
|
encoding: (null as unknown as string) // needs to be fixed in node-pty
|
||||||
|
});
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
let length = 0;
|
||||||
|
p.on('data', data => {
|
||||||
|
chunks.push(data as unknown as Buffer);
|
||||||
|
length += data.length;
|
||||||
|
});
|
||||||
|
await new Promise<void>(resolve => p.on('exit', () => resolve()));
|
||||||
|
contentUtf8 = Buffer.concat(chunks, length);
|
||||||
|
// translate to content string
|
||||||
|
const buffer = new Uint32Array(contentUtf8.length);
|
||||||
|
const decoder = new Utf8ToUtf32();
|
||||||
|
const codepoints = decoder.decode(contentUtf8, buffer);
|
||||||
|
for (let i = 0; i < codepoints; ++i) {
|
||||||
|
content += stringFromCodePoint(buffer[i]);
|
||||||
|
// peek into content to force flat repr in v8
|
||||||
|
if (!(i % 10000000)) {
|
||||||
|
content[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
perfContext('serialize', () => {
|
||||||
|
let terminal: TestTerminal;
|
||||||
|
const serializeAddon = new SerializeAddon();
|
||||||
|
before(() => {
|
||||||
|
terminal = new TestTerminal({ cols: 80, rows: 25, scrollback: 5000 });
|
||||||
|
serializeAddon.activate(terminal);
|
||||||
|
terminal.writeSync(content);
|
||||||
|
});
|
||||||
|
new ThroughputRuntimeCase('', () => {
|
||||||
|
return { payloadSize: serializeAddon.serialize().length };
|
||||||
|
}, { fork: false }).showAverageThroughput();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"APP_PATH": ".benchmark",
|
||||||
|
"evalConfig": {
|
||||||
|
"tolerance": {
|
||||||
|
"*": [0.75, 1.5],
|
||||||
|
"*.dev": [0.01, 1.5],
|
||||||
|
"*.cv": [0.01, 1.5],
|
||||||
|
"EscapeSequenceParser.benchmark.js.*.averageThroughput.mean": [0.9, 5]
|
||||||
|
},
|
||||||
|
"skip": [
|
||||||
|
"*.median",
|
||||||
|
"*.runs",
|
||||||
|
"*.dev",
|
||||||
|
"*.cv",
|
||||||
|
"EscapeSequenceParser.benchmark.js.*.averageRuntime",
|
||||||
|
"Terminal.benchmark.js.*.averageRuntime"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"lib": ["dom", "es6"],
|
||||||
|
"outDir": "../out-benchmark",
|
||||||
|
"types": ["../../../node_modules/@types/node"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"strict": false,
|
||||||
|
"target": "es2015",
|
||||||
|
"module": "commonjs",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"common/*": ["../../../src/common/*"],
|
||||||
|
"browser/*": ["../../../src/browser/*"],
|
||||||
|
"SerializeAddon": ["../src/SerializeAddon"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["../**/*", "../../../typings/xterm.d.ts", "../../../out/**/*"],
|
||||||
|
"exclude": ["../../../**/*test.ts", "../../**/*api.ts"],
|
||||||
|
"references": [
|
||||||
|
{ "path": "../../../src/common" },
|
||||||
|
{ "path": "../../../src/browser" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "xterm-addon-serialize",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"author": {
|
||||||
|
"name": "The xterm.js authors",
|
||||||
|
"url": "https://xtermjs.org/"
|
||||||
|
},
|
||||||
|
"main": "lib/xterm-addon-serialize.js",
|
||||||
|
"types": "typings/xterm-addon-serialize.d.ts",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
|
"prepackage": "npm run build",
|
||||||
|
"package": "../../node_modules/.bin/webpack",
|
||||||
|
"prepublishOnly": "npm run package",
|
||||||
|
"benchmark": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json",
|
||||||
|
"benchmark-baseline": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --baseline out-benchmark/benchmark/*benchmark.js",
|
||||||
|
"benchmark-eval": "NODE_PATH=../../out:./out:./out-benchmark/ ../../node_modules/.bin/xterm-benchmark -r 5 -c benchmark/benchmark.json --eval out-benchmark/benchmark/*benchmark.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* (EXPERIMENTAL) This Addon is still under development
|
||||||
|
*/
|
||||||
|
|
||||||
|
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) { }
|
||||||
|
|
||||||
|
public serialize(startRow: number, endRow: number): string {
|
||||||
|
// we need two of them to flip between old and new cell
|
||||||
|
const cell1 = this._buffer.getNullCell();
|
||||||
|
const cell2 = this._buffer.getNullCell();
|
||||||
|
let oldCell = cell1;
|
||||||
|
|
||||||
|
this._beforeSerialize(endRow - startRow, startRow, endRow);
|
||||||
|
|
||||||
|
for (let row = startRow; row < endRow; row++) {
|
||||||
|
const line = this._buffer.getLine(row);
|
||||||
|
if (line) {
|
||||||
|
for (let col = 0; col < line.length; col++) {
|
||||||
|
const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1);
|
||||||
|
if (!c) {
|
||||||
|
console.warn(`Can't get cell at row=${row}, col=${col}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._nextCell(c, oldCell, row, col);
|
||||||
|
oldCell = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._rowEnd(row, row === endRow - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._afterSerialize();
|
||||||
|
|
||||||
|
return this._serializeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { }
|
||||||
|
protected _rowEnd(row: number, isLastRow: boolean): void { }
|
||||||
|
protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { }
|
||||||
|
protected _afterSerialize(): void { }
|
||||||
|
protected _serializeString(): string { return ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalFg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||||
|
return cell1.getFgColorMode() === cell2.getFgColorMode()
|
||||||
|
&& cell1.getFgColor() === cell2.getFgColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalBg(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||||
|
return cell1.getBgColorMode() === cell2.getBgColorMode()
|
||||||
|
&& cell1.getBgColor() === cell2.getBgColor();
|
||||||
|
}
|
||||||
|
|
||||||
|
function equalFlags(cell1: IBufferCell, cell2: IBufferCell): boolean {
|
||||||
|
return cell1.isInverse() === cell2.isInverse()
|
||||||
|
&& cell1.isBold() === cell2.isBold()
|
||||||
|
&& cell1.isUnderline() === cell2.isUnderline()
|
||||||
|
&& cell1.isBlink() === cell2.isBlink()
|
||||||
|
&& cell1.isInvisible() === cell2.isInvisible()
|
||||||
|
&& cell1.isItalic() === cell2.isItalic()
|
||||||
|
&& cell1.isDim() === cell2.isDim();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// 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._buffer1.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._buffer1.getNullCell();
|
||||||
|
|
||||||
|
private _firstRow: number = 0;
|
||||||
|
private _lastCursorRow: number = 0;
|
||||||
|
private _lastCursorCol: number = 0;
|
||||||
|
private _lastContentCursorRow: number = 0;
|
||||||
|
private _lastContentCursorCol: number = 0;
|
||||||
|
|
||||||
|
constructor(private _buffer1: IBuffer, private _terminal: Terminal) {
|
||||||
|
super(_buffer1);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _beforeSerialize(rows: number, start: number, end: number): void {
|
||||||
|
this._allRows = new Array<string>(rows);
|
||||||
|
this._lastContentCursorRow = start;
|
||||||
|
this._lastCursorRow = start;
|
||||||
|
this._firstRow = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _thisRowLastChar: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
private _thisRowLastSecondChar: IBufferCell = this._buffer1.getNullCell();
|
||||||
|
private _nextRowFirstChar: IBufferCell = this._buffer1.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 += `\x1b[${this._nullCellCount}X`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rowSeparator = '';
|
||||||
|
|
||||||
|
// handle row separator
|
||||||
|
if (!isLastRow) {
|
||||||
|
// Enable BCE
|
||||||
|
if (row - this._firstRow >= this._terminal.rows) {
|
||||||
|
this._buffer1.getLine(this._cursorStyleRow)?.getCell(this._cursorStyleCol, this._backgroundCell);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch current line
|
||||||
|
const currentLine = this._buffer1.getLine(row)!;
|
||||||
|
// Fetch next line
|
||||||
|
const nextLine = this._buffer1.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 += '\x1b[1D\x1b[1X';
|
||||||
|
|
||||||
|
if (this._nullCellCount > 0) {
|
||||||
|
// do these because we filled the last several null slot, which we shouldn't
|
||||||
|
rowSeparator += '\x1b[A';
|
||||||
|
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}C`;
|
||||||
|
rowSeparator += `\x1b[${this._nullCellCount}X`;
|
||||||
|
rowSeparator += `\x1b[${currentLine.length - this._nullCellCount}D`;
|
||||||
|
rowSeparator += '\x1b[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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _diffStyle(cell: IBufferCell, oldCell: IBufferCell): number[] {
|
||||||
|
const sgrSeq: number[] = [];
|
||||||
|
const fgChanged = !equalFg(cell, oldCell);
|
||||||
|
const bgChanged = !equalBg(cell, oldCell);
|
||||||
|
const flagsChanged = !equalFlags(cell, oldCell);
|
||||||
|
|
||||||
|
if (fgChanged || bgChanged || flagsChanged) {
|
||||||
|
if (cell.isAttributeDefault()) {
|
||||||
|
if (!oldCell.isAttributeDefault()) {
|
||||||
|
sgrSeq.push(0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (fgChanged) {
|
||||||
|
const color = cell.getFgColor();
|
||||||
|
if (cell.isFgRGB()) { sgrSeq.push(38, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||||
|
else if (cell.isFgPalette()) {
|
||||||
|
if (color >= 16) { sgrSeq.push(38, 5, color); }
|
||||||
|
else { sgrSeq.push(color & 8 ? 90 + (color & 7) : 30 + (color & 7)); }
|
||||||
|
}
|
||||||
|
else { sgrSeq.push(39); }
|
||||||
|
}
|
||||||
|
if (bgChanged) {
|
||||||
|
const color = cell.getBgColor();
|
||||||
|
if (cell.isBgRGB()) { sgrSeq.push(48, 2, (color >>> 16) & 0xFF, (color >>> 8) & 0xFF, color & 0xFF); }
|
||||||
|
else if (cell.isBgPalette()) {
|
||||||
|
if (color >= 16) { sgrSeq.push(48, 5, color); }
|
||||||
|
else { sgrSeq.push(color & 8 ? 100 + (color & 7) : 40 + (color & 7)); }
|
||||||
|
}
|
||||||
|
else { sgrSeq.push(49); }
|
||||||
|
}
|
||||||
|
if (flagsChanged) {
|
||||||
|
if (cell.isInverse() !== oldCell.isInverse()) { sgrSeq.push(cell.isInverse() ? 7 : 27); }
|
||||||
|
if (cell.isBold() !== oldCell.isBold()) { sgrSeq.push(cell.isBold() ? 1 : 22); }
|
||||||
|
if (cell.isUnderline() !== oldCell.isUnderline()) { sgrSeq.push(cell.isUnderline() ? 4 : 24); }
|
||||||
|
if (cell.isBlink() !== oldCell.isBlink()) { sgrSeq.push(cell.isBlink() ? 5 : 25); }
|
||||||
|
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); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 += `\x1b[${this._nullCellCount}X`;
|
||||||
|
}
|
||||||
|
// use move right to move cursor.
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||||
|
this._nullCellCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._lastContentCursorRow = this._lastCursorRow = row;
|
||||||
|
this._lastContentCursorCol = this._lastCursorCol = col;
|
||||||
|
|
||||||
|
this._currentRow += `\x1b[${sgrSeq.join(';')}m`;
|
||||||
|
|
||||||
|
// update the last cursor style
|
||||||
|
const line = this._buffer1.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) {
|
||||||
|
// 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 += `\x1b[${this._nullCellCount}C`;
|
||||||
|
} else {
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}X`;
|
||||||
|
this._currentRow += `\x1b[${this._nullCellCount}C`;
|
||||||
|
}
|
||||||
|
this._nullCellCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// the fixup is only required for data without scrollback
|
||||||
|
// because it will always be placed at last line otherwise
|
||||||
|
if (this._buffer1.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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// restore the cursor
|
||||||
|
const realCursorRow = this._buffer1.baseY + this._buffer1.cursorY;
|
||||||
|
const realCursorCol = this._buffer1.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SerializeAddon implements ITerminalAddon {
|
||||||
|
private _terminal: Terminal | undefined;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
public activate(terminal: Terminal): void {
|
||||||
|
this._terminal = terminal;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getString(buffer: IBuffer, scrollback?: number): string {
|
||||||
|
const maxRows = buffer.length;
|
||||||
|
const handler = new StringSerializeHandler(buffer, this._terminal!);
|
||||||
|
|
||||||
|
const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + this!._terminal!.rows, 0, maxRows);
|
||||||
|
const result = handler.serialize(maxRows - correctRows, maxRows);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public serialize(scrollback?: number): string {
|
||||||
|
// TODO: Add combinedData support
|
||||||
|
if (!this._terminal) {
|
||||||
|
throw new Error('Cannot use addon until it has been loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._terminal.buffer.active.type === 'normal') {
|
||||||
|
return this._getString(this._terminal.buffer.active, scrollback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalScreenContent = this._getString(this._terminal.buffer.normal, scrollback);
|
||||||
|
// alt screen don't have scrollback
|
||||||
|
const alternativeScreenContent = this._getString(this._terminal.buffer.alternate, undefined);
|
||||||
|
|
||||||
|
return normalScreenContent
|
||||||
|
+ '\u001b[?1049h\u001b[H'
|
||||||
|
+ alternativeScreenContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"common/*": [
|
||||||
|
"../../../src/common/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../src/common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,551 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { openTerminal, writeSync, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||||
|
import { Browser, Page } from 'playwright';
|
||||||
|
|
||||||
|
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));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('SerializeAddon', () => {
|
||||||
|
before(async function(): Promise<any> {
|
||||||
|
const browserType = getBrowserType();
|
||||||
|
browser = await browserType.launch({
|
||||||
|
headless: process.argv.includes('--headless')
|
||||||
|
});
|
||||||
|
page = await (await browser.newContext()).newPage();
|
||||||
|
await page.setViewportSize({ width, height });
|
||||||
|
await page.goto(APP);
|
||||||
|
await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' });
|
||||||
|
await page.evaluate(`
|
||||||
|
window.serializeAddon = new SerializeAddon();
|
||||||
|
window.term.loadAddon(window.serializeAddon);
|
||||||
|
window.inspectBuffer = (buffer) => {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < buffer.length; i++) {
|
||||||
|
// Do this intentionally to get content of underlining source
|
||||||
|
const bufferLine = buffer.getLine(i)._line;
|
||||||
|
lines.push(JSON.stringify(bufferLine));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
x: buffer.cursorX,
|
||||||
|
y: buffer.cursorY,
|
||||||
|
data: lines
|
||||||
|
};
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => await browser.close());
|
||||||
|
beforeEach(async () => await page.evaluate(`window.term.reset()`));
|
||||||
|
|
||||||
|
it('produce different output when we call test util with different text', async function(): Promise<any> {
|
||||||
|
await writeRawSync(page, '12345');
|
||||||
|
const buffer1 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
await page.evaluate(`term.reset();`);
|
||||||
|
await writeRawSync(page, '67890');
|
||||||
|
const buffer2 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
assert.throw(() => {
|
||||||
|
assert.equal(JSON.stringify(buffer1), JSON.stringify(buffer2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('produce different output when we call test util with different line wrap', async function(): Promise<any> {
|
||||||
|
await writeRawSync(page, '1234567890\r\n12345');
|
||||||
|
const buffer3 = await page.evaluate(`inspectBuffer(term.buffer.normal);`);
|
||||||
|
|
||||||
|
await page.evaluate(`term.reset();`);
|
||||||
|
await writeRawSync(page, '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('unwrap wrapped line', async function(): Promise<any> {
|
||||||
|
const lines = ['123456789123456789'];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not unwrap non-wrapped line', async function(): Promise<any> {
|
||||||
|
const lines = [
|
||||||
|
'123456789',
|
||||||
|
'123456789'
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('preserve last empty lines', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const lines = [
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
digitsString(cols),
|
||||||
|
digitsString(cols),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
digitsString(cols),
|
||||||
|
digitsString(cols),
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('digits content', async function(): Promise<any> {
|
||||||
|
const rows = 10;
|
||||||
|
const cols = 10;
|
||||||
|
const digitsLine = digitsString(cols);
|
||||||
|
const lines = newArray<string>(digitsLine, rows);
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize with half of scrollback', async function(): Promise<any> {
|
||||||
|
const rows = 20;
|
||||||
|
const scrollback = rows - 10;
|
||||||
|
const halfScrollback = scrollback / 2;
|
||||||
|
const cols = 10;
|
||||||
|
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize(${halfScrollback});`), lines.slice(halfScrollback, rows).join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize 0 rows of scrollback', async function(): Promise<any> {
|
||||||
|
const rows = 20;
|
||||||
|
const cols = 10;
|
||||||
|
const lines = newArray<string>((index: number) => digitsString(cols, index), rows);
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize(0);`), lines.slice(rows - 10, rows).join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color16', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const color16 = [
|
||||||
|
30, 31, 32, 33, 34, 35, 36, 37, // Set foreground color
|
||||||
|
90, 91, 92, 93, 94, 95, 96, 97,
|
||||||
|
40, 41, 42, 43, 44, 45, 46, 47, // Set background color
|
||||||
|
100, 101, 103, 104, 105, 106, 107
|
||||||
|
];
|
||||||
|
const rows = color16.length;
|
||||||
|
const lines = newArray<string>(
|
||||||
|
(index: number) => digitsString(cols, index, `\x1b[${color16[index % color16.length]}m`),
|
||||||
|
rows
|
||||||
|
);
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with fg/bg flags', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
|
||||||
|
mkSGR(INVERSE) + line,
|
||||||
|
mkSGR(BOLD) + line,
|
||||||
|
mkSGR(UNDERLINED) + line,
|
||||||
|
mkSGR(BLINK) + line,
|
||||||
|
mkSGR(INVISIBLE) + line,
|
||||||
|
mkSGR(NO_INVERSE) + line,
|
||||||
|
mkSGR(NO_BOLD) + line,
|
||||||
|
mkSGR(NO_UNDERLINED) + line,
|
||||||
|
mkSGR(NO_BLINK) + line,
|
||||||
|
mkSGR(NO_INVISIBLE) + line
|
||||||
|
];
|
||||||
|
const rows = lines.length;
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color256', async function(): Promise<any> {
|
||||||
|
const rows = 32;
|
||||||
|
const cols = 10;
|
||||||
|
const lines = newArray<string>(
|
||||||
|
(index: number) => digitsString(cols, index, `\x1b[38;5;${16 + index}m`),
|
||||||
|
rows
|
||||||
|
);
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color16 and style separately', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_P16_RED) + line, // fg Red,
|
||||||
|
mkSGR(UNDERLINED) + line, // fg Red, Underlined
|
||||||
|
mkSGR(FG_P16_GREEN) + line, // fg Green, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||||
|
mkSGR(NO_INVERSE) + line, // fg Green, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green, Underlined, Inverse
|
||||||
|
mkSGR(BG_P16_YELLOW) + line, // fg Green, bg Yellow, Underlined, Inverse
|
||||||
|
mkSGR(FG_RESET) + line, // bg Yellow, Underlined, Inverse
|
||||||
|
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||||
|
mkSGR(NORMAL) + line // Back to normal
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color16 and style together', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_P16_RED) + line, // fg Red
|
||||||
|
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||||
|
mkSGR(BG_RESET) + line, // Italic
|
||||||
|
mkSGR(NORMAL) + line, // Back to normal
|
||||||
|
mkSGR(FG_P16_RED) + line, // fg Red
|
||||||
|
mkSGR(FG_P16_GREEN, BG_P16_YELLOW) + line, // fg Green, bg Yellow
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green, bg Yellow, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green, bg Yellow
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow, Italic
|
||||||
|
mkSGR(BG_RESET) + line // Italic
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color256 and style separately', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_P256_RED) + line, // fg Red 256,
|
||||||
|
mkSGR(UNDERLINED) + line, // fg Red 256, Underlined
|
||||||
|
mkSGR(FG_P256_GREEN) + line, // fg Green 256, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||||
|
mkSGR(NO_INVERSE) + line, // fg Green 256, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green 256, Underlined, Inverse
|
||||||
|
mkSGR(BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256, Underlined, Inverse
|
||||||
|
mkSGR(FG_RESET) + line, // bg Yellow 256, Underlined, Inverse
|
||||||
|
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||||
|
mkSGR(NORMAL) + line // Back to normal
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with color256 and style together', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_P256_RED) + line, // fg Red 256
|
||||||
|
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||||
|
mkSGR(BG_RESET) + line, // Italic
|
||||||
|
mkSGR(NORMAL) + line, // Back to normal
|
||||||
|
mkSGR(FG_P256_RED) + line, // fg Red 256
|
||||||
|
mkSGR(FG_P256_GREEN, BG_P256_YELLOW) + line, // fg Green 256, bg Yellow 256
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green 256, bg Yellow 256, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green 256, bg Yellow 256
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow 256, Italic
|
||||||
|
mkSGR(BG_RESET) + line // Italic
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with colorRGB and style separately', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_RGB_RED) + line, // fg Red RGB,
|
||||||
|
mkSGR(UNDERLINED) + line, // fg Red RGB, Underlined
|
||||||
|
mkSGR(FG_RGB_GREEN) + line, // fg Green RGB, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||||
|
mkSGR(NO_INVERSE) + line, // fg Green RGB, Underlined
|
||||||
|
mkSGR(INVERSE) + line, // fg Green RGB, Underlined, Inverse
|
||||||
|
mkSGR(BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB, Underlined, Inverse
|
||||||
|
mkSGR(FG_RESET) + line, // bg Yellow RGB, Underlined, Inverse
|
||||||
|
mkSGR(BG_RESET) + line, // Underlined, Inverse
|
||||||
|
mkSGR(NORMAL) + line // Back to normal
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize all rows of content with colorRGB and style together', async function(): Promise<any> {
|
||||||
|
const cols = 10;
|
||||||
|
const line = '+'.repeat(cols);
|
||||||
|
const lines: string[] = [
|
||||||
|
mkSGR(FG_RGB_RED) + line, // fg Red RGB
|
||||||
|
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||||
|
mkSGR(BG_RESET) + line, // Italic
|
||||||
|
mkSGR(NORMAL) + line, // Back to normal
|
||||||
|
mkSGR(FG_RGB_RED) + line, // fg Red RGB
|
||||||
|
mkSGR(FG_RGB_GREEN, BG_RGB_YELLOW) + line, // fg Green RGB, bg Yellow RGB
|
||||||
|
mkSGR(UNDERLINED, ITALIC) + line, // fg Green RGB, bg Yellow RGB, Underlined, Italic
|
||||||
|
mkSGR(NO_UNDERLINED, NO_ITALIC) + line, // fg Green RGB, bg Yellow RGB
|
||||||
|
mkSGR(FG_RESET, ITALIC) + line, // bg Yellow RGB, Italic
|
||||||
|
mkSGR(BG_RESET) + line // Italic
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize tabs correctly', async () => {
|
||||||
|
const lines = [
|
||||||
|
'a\tb',
|
||||||
|
'aa\tc',
|
||||||
|
'aaa\td'
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
'a\x1b[7Cb',
|
||||||
|
'aa\x1b[6Cc',
|
||||||
|
'aaa\x1b[5Cd'
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize CJK correctly', async () => {
|
||||||
|
const lines = [
|
||||||
|
'中文中文',
|
||||||
|
'12中文',
|
||||||
|
'中文12',
|
||||||
|
// This line is going to be wrapped at last character
|
||||||
|
// because it has line length of 11 (1+2*5).
|
||||||
|
// We concat it back without the null cell currently.
|
||||||
|
// But this may be incorrect.
|
||||||
|
// see also #3097
|
||||||
|
'1中文中文中'
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize CJK Mixed with tab correctly', async () => {
|
||||||
|
const lines = [
|
||||||
|
'中文\t12' // CJK mixed with tab
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
'中文\x1b[4C12'
|
||||||
|
];
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`serializeAddon.serialize();`), expected.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize with alt screen correctly', async () => {
|
||||||
|
const SMCUP = '\u001b[?1049h';
|
||||||
|
const CUP = '\u001b[H';
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1${SMCUP}${CUP}2`
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
`1${SMCUP}${CUP}2`
|
||||||
|
];
|
||||||
|
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'alternate');
|
||||||
|
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize without alt screen correctly', async () => {
|
||||||
|
const SMCUP = '\u001b[?1049h';
|
||||||
|
const RMCUP = '\u001b[?1049l';
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1${SMCUP}2${RMCUP}`
|
||||||
|
];
|
||||||
|
const expected = [
|
||||||
|
`1`
|
||||||
|
];
|
||||||
|
|
||||||
|
await writeSync(page, lines.join('\\r\\n'));
|
||||||
|
assert.equal(await page.evaluate(`window.term.buffer.active.type`), 'normal');
|
||||||
|
assert.equal(JSON.stringify(await page.evaluate(`serializeAddon.serialize();`)), JSON.stringify(expected.join('\r\n')));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('serialize with background', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
`1\u001b[44m${CLEAR_RIGHT(5)}`,
|
||||||
|
`2${CLEAR_RIGHT(9)}`
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('cause the BCE on scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
|
||||||
|
const padLines = newArray<string>(
|
||||||
|
(index: number) => digitsString(10, index),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
...padLines,
|
||||||
|
`\u001b[44m${CLEAR_RIGHT(5)}1111111111111111`
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join('\r\n'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle invalid wrap before scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||||
|
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||||
|
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||||
|
|
||||||
|
// A line wrap happened after current line.
|
||||||
|
// But there is no content.
|
||||||
|
// so wrap shouldn't even be able to happen.
|
||||||
|
const segments = [
|
||||||
|
`123456789012345`,
|
||||||
|
MOVE_UP(1),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_DOWN(1),
|
||||||
|
MOVE_LEFT(5),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_UP(1),
|
||||||
|
'1'
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, segments.join(''));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handle invalid wrap after scroll', async () => {
|
||||||
|
const CLEAR_RIGHT = (l: number): string => `\u001b[${l}X`;
|
||||||
|
const MOVE_UP = (l: number): string => `\u001b[${l}A`;
|
||||||
|
const MOVE_DOWN = (l: number): string => `\u001b[${l}B`;
|
||||||
|
const MOVE_LEFT = (l: number): string => `\u001b[${l}D`;
|
||||||
|
|
||||||
|
const padLines = newArray<string>(
|
||||||
|
(index: number) => digitsString(10, index),
|
||||||
|
10
|
||||||
|
);
|
||||||
|
|
||||||
|
// A line wrap happened after current line.
|
||||||
|
// But there is no content.
|
||||||
|
// so wrap shouldn't even be able to happen.
|
||||||
|
const lines = [
|
||||||
|
padLines.join('\r\n'),
|
||||||
|
'\r\n',
|
||||||
|
`123456789012345`,
|
||||||
|
MOVE_UP(1),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_DOWN(1),
|
||||||
|
MOVE_LEFT(5),
|
||||||
|
CLEAR_RIGHT(5),
|
||||||
|
MOVE_UP(1),
|
||||||
|
'1'
|
||||||
|
];
|
||||||
|
|
||||||
|
await testNormalScreenEqual(page, lines.join(''));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function newArray<T>(initial: T | ((index: number) => T), count: number): T[] {
|
||||||
|
const array: T[] = new Array<T>(count);
|
||||||
|
for (let i = 0; i < array.length; i++) {
|
||||||
|
if (typeof initial === 'function') {
|
||||||
|
array[i] = (initial as (index: number) => T)(i);
|
||||||
|
} else {
|
||||||
|
array[i] = initial as T;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
function digitsString(length: number, from: number = 0, sgr: string = ''): string {
|
||||||
|
let s = sgr;
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
s += `${(from++) % 10}`;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkSGR(...seq: string[]): string {
|
||||||
|
return `\x1b[${seq.join(';')}m`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NORMAL = '0';
|
||||||
|
|
||||||
|
const FG_P16_RED = '31';
|
||||||
|
const FG_P16_GREEN = '32';
|
||||||
|
const FG_P16_YELLOW = '33';
|
||||||
|
const FG_P256_RED = '38;5;196';
|
||||||
|
const FG_P256_GREEN = '38;5;46';
|
||||||
|
const FG_P256_YELLOW = '38;5;226';
|
||||||
|
const FG_RGB_RED = '38;2;255;0;0';
|
||||||
|
const FG_RGB_GREEN = '38;2;0;255;0';
|
||||||
|
const FG_RGB_YELLOW = '38;2;255;255;0';
|
||||||
|
const FG_RESET = '39';
|
||||||
|
|
||||||
|
|
||||||
|
const BG_P16_RED = '41';
|
||||||
|
const BG_P16_GREEN = '42';
|
||||||
|
const BG_P16_YELLOW = '43';
|
||||||
|
const BG_P256_RED = '48;5;196';
|
||||||
|
const BG_P256_GREEN = '48;5;46';
|
||||||
|
const BG_P256_YELLOW = '48;5;226';
|
||||||
|
const BG_RGB_RED = '48;2;255;0;0';
|
||||||
|
const BG_RGB_GREEN = '48;2;0;255;0';
|
||||||
|
const BG_RGB_YELLOW = '48;2;255;255;0';
|
||||||
|
const BG_RESET = '49';
|
||||||
|
|
||||||
|
const INVERSE = '7';
|
||||||
|
const BOLD = '1';
|
||||||
|
const UNDERLINED = '4';
|
||||||
|
const BLINK = '5';
|
||||||
|
const INVISIBLE = '8';
|
||||||
|
|
||||||
|
const NO_INVERSE = '27';
|
||||||
|
const NO_BOLD = '22';
|
||||||
|
const NO_UNDERLINED = '24';
|
||||||
|
const NO_BLINK = '25';
|
||||||
|
const NO_INVISIBLE = '28';
|
||||||
|
|
||||||
|
const ITALIC = '3';
|
||||||
|
const DIM = '2';
|
||||||
|
|
||||||
|
const NO_ITALIC = '23';
|
||||||
|
const NO_DIM = '22';
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"common/*": [
|
||||||
|
"../../../src/common/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../src/common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" },
|
||||||
|
{ "path": "./benchmark" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm';
|
||||||
|
|
||||||
|
declare module 'xterm-addon-serialize' {
|
||||||
|
/**
|
||||||
|
* An xterm.js addon that enables web links.
|
||||||
|
*/
|
||||||
|
export class SerializeAddon implements ITerminalAddon {
|
||||||
|
|
||||||
|
constructor();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 scrollback The number of rows in scrollback buffer to serialize, starting from the bottom of the
|
||||||
|
* scrollback buffer. This defaults to the all available rows in the scrollback buffer.
|
||||||
|
*/
|
||||||
|
public serialize(scrollback?: number): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes the addon.
|
||||||
|
*/
|
||||||
|
public dispose(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const addonName = 'SerializeAddon';
|
||||||
|
const mainFile = 'xterm-addon-serialize.js';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: `./out/${addonName}.js`,
|
||||||
|
devtool: 'source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre",
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: mainFile,
|
||||||
|
path: path.resolve('./lib'),
|
||||||
|
library: addonName,
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
mode: 'production'
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
lib
|
||||||
|
node_modules
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
**/*.api.js
|
||||||
|
**/*.api.ts
|
||||||
|
tsconfig.json
|
||||||
|
.yarnrc
|
||||||
|
webpack.config.js
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2019, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
## xterm-addon-unicode11
|
||||||
|
|
||||||
|
An addon providing Unicode version 11 rules for xterm.js.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install --save xterm-addon-unicode11
|
||||||
|
```
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import { Unicode11Addon } from 'xterm-addon-unicode11';
|
||||||
|
|
||||||
|
const terminal = new Terminal();
|
||||||
|
const unicode11Addon = new Unicode11Addon();
|
||||||
|
terminal.loadAddon(unicode11Addon);
|
||||||
|
|
||||||
|
// activate the new version
|
||||||
|
terminal.unicode.activeVersion = '11';
|
||||||
|
```
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "xterm-addon-unicode11",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"author": {
|
||||||
|
"name": "The xterm.js authors",
|
||||||
|
"url": "https://xtermjs.org/"
|
||||||
|
},
|
||||||
|
"main": "lib/xterm-addon-unicode11.js",
|
||||||
|
"types": "typings/xterm-addon-unicode11.d.ts",
|
||||||
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
|
"prepackage": "npm run build",
|
||||||
|
"package": "../../node_modules/.bin/webpack",
|
||||||
|
"prepublishOnly": "npm run package"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^4.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*
|
||||||
|
* UnicodeVersionProvider for V11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm';
|
||||||
|
import { UnicodeV11 } from './UnicodeV11';
|
||||||
|
|
||||||
|
export class Unicode11Addon implements ITerminalAddon {
|
||||||
|
public activate(terminal: Terminal): void {
|
||||||
|
terminal.unicode.register(new UnicodeV11());
|
||||||
|
}
|
||||||
|
public dispose(): void { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { IUnicodeVersionProvider } from 'xterm';
|
||||||
|
import { fill } from 'common/TypedArrayUtils';
|
||||||
|
|
||||||
|
type CharWidth = 0 | 1 | 2;
|
||||||
|
|
||||||
|
const BMP_COMBINING = [
|
||||||
|
[0x0300, 0x036F], [0x0483, 0x0489], [0x0591, 0x05BD],
|
||||||
|
[0x05BF, 0x05BF], [0x05C1, 0x05C2], [0x05C4, 0x05C5],
|
||||||
|
[0x05C7, 0x05C7], [0x0600, 0x0605], [0x0610, 0x061A],
|
||||||
|
[0x061C, 0x061C], [0x064B, 0x065F], [0x0670, 0x0670],
|
||||||
|
[0x06D6, 0x06DD], [0x06DF, 0x06E4], [0x06E7, 0x06E8],
|
||||||
|
[0x06EA, 0x06ED], [0x070F, 0x070F], [0x0711, 0x0711],
|
||||||
|
[0x0730, 0x074A], [0x07A6, 0x07B0], [0x07EB, 0x07F3],
|
||||||
|
[0x07FD, 0x07FD], [0x0816, 0x0819], [0x081B, 0x0823],
|
||||||
|
[0x0825, 0x0827], [0x0829, 0x082D], [0x0859, 0x085B],
|
||||||
|
[0x08D3, 0x0902], [0x093A, 0x093A], [0x093C, 0x093C],
|
||||||
|
[0x0941, 0x0948], [0x094D, 0x094D], [0x0951, 0x0957],
|
||||||
|
[0x0962, 0x0963], [0x0981, 0x0981], [0x09BC, 0x09BC],
|
||||||
|
[0x09C1, 0x09C4], [0x09CD, 0x09CD], [0x09E2, 0x09E3],
|
||||||
|
[0x09FE, 0x09FE], [0x0A01, 0x0A02], [0x0A3C, 0x0A3C],
|
||||||
|
[0x0A41, 0x0A42], [0x0A47, 0x0A48], [0x0A4B, 0x0A4D],
|
||||||
|
[0x0A51, 0x0A51], [0x0A70, 0x0A71], [0x0A75, 0x0A75],
|
||||||
|
[0x0A81, 0x0A82], [0x0ABC, 0x0ABC], [0x0AC1, 0x0AC5],
|
||||||
|
[0x0AC7, 0x0AC8], [0x0ACD, 0x0ACD], [0x0AE2, 0x0AE3],
|
||||||
|
[0x0AFA, 0x0AFF], [0x0B01, 0x0B01], [0x0B3C, 0x0B3C],
|
||||||
|
[0x0B3F, 0x0B3F], [0x0B41, 0x0B44], [0x0B4D, 0x0B4D],
|
||||||
|
[0x0B56, 0x0B56], [0x0B62, 0x0B63], [0x0B82, 0x0B82],
|
||||||
|
[0x0BC0, 0x0BC0], [0x0BCD, 0x0BCD], [0x0C00, 0x0C00],
|
||||||
|
[0x0C04, 0x0C04], [0x0C3E, 0x0C40], [0x0C46, 0x0C48],
|
||||||
|
[0x0C4A, 0x0C4D], [0x0C55, 0x0C56], [0x0C62, 0x0C63],
|
||||||
|
[0x0C81, 0x0C81], [0x0CBC, 0x0CBC], [0x0CBF, 0x0CBF],
|
||||||
|
[0x0CC6, 0x0CC6], [0x0CCC, 0x0CCD], [0x0CE2, 0x0CE3],
|
||||||
|
[0x0D00, 0x0D01], [0x0D3B, 0x0D3C], [0x0D41, 0x0D44],
|
||||||
|
[0x0D4D, 0x0D4D], [0x0D62, 0x0D63], [0x0DCA, 0x0DCA],
|
||||||
|
[0x0DD2, 0x0DD4], [0x0DD6, 0x0DD6], [0x0E31, 0x0E31],
|
||||||
|
[0x0E34, 0x0E3A], [0x0E47, 0x0E4E], [0x0EB1, 0x0EB1],
|
||||||
|
[0x0EB4, 0x0EBC], [0x0EC8, 0x0ECD], [0x0F18, 0x0F19],
|
||||||
|
[0x0F35, 0x0F35], [0x0F37, 0x0F37], [0x0F39, 0x0F39],
|
||||||
|
[0x0F71, 0x0F7E], [0x0F80, 0x0F84], [0x0F86, 0x0F87],
|
||||||
|
[0x0F8D, 0x0F97], [0x0F99, 0x0FBC], [0x0FC6, 0x0FC6],
|
||||||
|
[0x102D, 0x1030], [0x1032, 0x1037], [0x1039, 0x103A],
|
||||||
|
[0x103D, 0x103E], [0x1058, 0x1059], [0x105E, 0x1060],
|
||||||
|
[0x1071, 0x1074], [0x1082, 0x1082], [0x1085, 0x1086],
|
||||||
|
[0x108D, 0x108D], [0x109D, 0x109D], [0x1160, 0x11FF],
|
||||||
|
[0x135D, 0x135F], [0x1712, 0x1714], [0x1732, 0x1734],
|
||||||
|
[0x1752, 0x1753], [0x1772, 0x1773], [0x17B4, 0x17B5],
|
||||||
|
[0x17B7, 0x17BD], [0x17C6, 0x17C6], [0x17C9, 0x17D3],
|
||||||
|
[0x17DD, 0x17DD], [0x180B, 0x180E], [0x1885, 0x1886],
|
||||||
|
[0x18A9, 0x18A9], [0x1920, 0x1922], [0x1927, 0x1928],
|
||||||
|
[0x1932, 0x1932], [0x1939, 0x193B], [0x1A17, 0x1A18],
|
||||||
|
[0x1A1B, 0x1A1B], [0x1A56, 0x1A56], [0x1A58, 0x1A5E],
|
||||||
|
[0x1A60, 0x1A60], [0x1A62, 0x1A62], [0x1A65, 0x1A6C],
|
||||||
|
[0x1A73, 0x1A7C], [0x1A7F, 0x1A7F], [0x1AB0, 0x1ABE],
|
||||||
|
[0x1B00, 0x1B03], [0x1B34, 0x1B34], [0x1B36, 0x1B3A],
|
||||||
|
[0x1B3C, 0x1B3C], [0x1B42, 0x1B42], [0x1B6B, 0x1B73],
|
||||||
|
[0x1B80, 0x1B81], [0x1BA2, 0x1BA5], [0x1BA8, 0x1BA9],
|
||||||
|
[0x1BAB, 0x1BAD], [0x1BE6, 0x1BE6], [0x1BE8, 0x1BE9],
|
||||||
|
[0x1BED, 0x1BED], [0x1BEF, 0x1BF1], [0x1C2C, 0x1C33],
|
||||||
|
[0x1C36, 0x1C37], [0x1CD0, 0x1CD2], [0x1CD4, 0x1CE0],
|
||||||
|
[0x1CE2, 0x1CE8], [0x1CED, 0x1CED], [0x1CF4, 0x1CF4],
|
||||||
|
[0x1CF8, 0x1CF9], [0x1DC0, 0x1DF9], [0x1DFB, 0x1DFF],
|
||||||
|
[0x200B, 0x200F], [0x202A, 0x202E], [0x2060, 0x2064],
|
||||||
|
[0x2066, 0x206F], [0x20D0, 0x20F0], [0x2CEF, 0x2CF1],
|
||||||
|
[0x2D7F, 0x2D7F], [0x2DE0, 0x2DFF], [0x302A, 0x302D],
|
||||||
|
[0x3099, 0x309A], [0xA66F, 0xA672], [0xA674, 0xA67D],
|
||||||
|
[0xA69E, 0xA69F], [0xA6F0, 0xA6F1], [0xA802, 0xA802],
|
||||||
|
[0xA806, 0xA806], [0xA80B, 0xA80B], [0xA825, 0xA826],
|
||||||
|
[0xA8C4, 0xA8C5], [0xA8E0, 0xA8F1], [0xA8FF, 0xA8FF],
|
||||||
|
[0xA926, 0xA92D], [0xA947, 0xA951], [0xA980, 0xA982],
|
||||||
|
[0xA9B3, 0xA9B3], [0xA9B6, 0xA9B9], [0xA9BC, 0xA9BD],
|
||||||
|
[0xA9E5, 0xA9E5], [0xAA29, 0xAA2E], [0xAA31, 0xAA32],
|
||||||
|
[0xAA35, 0xAA36], [0xAA43, 0xAA43], [0xAA4C, 0xAA4C],
|
||||||
|
[0xAA7C, 0xAA7C], [0xAAB0, 0xAAB0], [0xAAB2, 0xAAB4],
|
||||||
|
[0xAAB7, 0xAAB8], [0xAABE, 0xAABF], [0xAAC1, 0xAAC1],
|
||||||
|
[0xAAEC, 0xAAED], [0xAAF6, 0xAAF6], [0xABE5, 0xABE5],
|
||||||
|
[0xABE8, 0xABE8], [0xABED, 0xABED], [0xFB1E, 0xFB1E],
|
||||||
|
[0xFE00, 0xFE0F], [0xFE20, 0xFE2F], [0xFEFF, 0xFEFF],
|
||||||
|
[0xFFF9, 0xFFFB]
|
||||||
|
];
|
||||||
|
const HIGH_COMBINING = [
|
||||||
|
[0x101FD, 0x101FD], [0x102E0, 0x102E0],
|
||||||
|
[0x10376, 0x1037A], [0x10A01, 0x10A03], [0x10A05, 0x10A06],
|
||||||
|
[0x10A0C, 0x10A0F], [0x10A38, 0x10A3A], [0x10A3F, 0x10A3F],
|
||||||
|
[0x10AE5, 0x10AE6], [0x10D24, 0x10D27], [0x10F46, 0x10F50],
|
||||||
|
[0x11001, 0x11001], [0x11038, 0x11046], [0x1107F, 0x11081],
|
||||||
|
[0x110B3, 0x110B6], [0x110B9, 0x110BA], [0x110BD, 0x110BD],
|
||||||
|
[0x110CD, 0x110CD], [0x11100, 0x11102], [0x11127, 0x1112B],
|
||||||
|
[0x1112D, 0x11134], [0x11173, 0x11173], [0x11180, 0x11181],
|
||||||
|
[0x111B6, 0x111BE], [0x111C9, 0x111CC], [0x1122F, 0x11231],
|
||||||
|
[0x11234, 0x11234], [0x11236, 0x11237], [0x1123E, 0x1123E],
|
||||||
|
[0x112DF, 0x112DF], [0x112E3, 0x112EA], [0x11300, 0x11301],
|
||||||
|
[0x1133B, 0x1133C], [0x11340, 0x11340], [0x11366, 0x1136C],
|
||||||
|
[0x11370, 0x11374], [0x11438, 0x1143F], [0x11442, 0x11444],
|
||||||
|
[0x11446, 0x11446], [0x1145E, 0x1145E], [0x114B3, 0x114B8],
|
||||||
|
[0x114BA, 0x114BA], [0x114BF, 0x114C0], [0x114C2, 0x114C3],
|
||||||
|
[0x115B2, 0x115B5], [0x115BC, 0x115BD], [0x115BF, 0x115C0],
|
||||||
|
[0x115DC, 0x115DD], [0x11633, 0x1163A], [0x1163D, 0x1163D],
|
||||||
|
[0x1163F, 0x11640], [0x116AB, 0x116AB], [0x116AD, 0x116AD],
|
||||||
|
[0x116B0, 0x116B5], [0x116B7, 0x116B7], [0x1171D, 0x1171F],
|
||||||
|
[0x11722, 0x11725], [0x11727, 0x1172B], [0x1182F, 0x11837],
|
||||||
|
[0x11839, 0x1183A], [0x119D4, 0x119D7], [0x119DA, 0x119DB],
|
||||||
|
[0x119E0, 0x119E0], [0x11A01, 0x11A0A], [0x11A33, 0x11A38],
|
||||||
|
[0x11A3B, 0x11A3E], [0x11A47, 0x11A47], [0x11A51, 0x11A56],
|
||||||
|
[0x11A59, 0x11A5B], [0x11A8A, 0x11A96], [0x11A98, 0x11A99],
|
||||||
|
[0x11C30, 0x11C36], [0x11C38, 0x11C3D], [0x11C3F, 0x11C3F],
|
||||||
|
[0x11C92, 0x11CA7], [0x11CAA, 0x11CB0], [0x11CB2, 0x11CB3],
|
||||||
|
[0x11CB5, 0x11CB6], [0x11D31, 0x11D36], [0x11D3A, 0x11D3A],
|
||||||
|
[0x11D3C, 0x11D3D], [0x11D3F, 0x11D45], [0x11D47, 0x11D47],
|
||||||
|
[0x11D90, 0x11D91], [0x11D95, 0x11D95], [0x11D97, 0x11D97],
|
||||||
|
[0x11EF3, 0x11EF4], [0x13430, 0x13438], [0x16AF0, 0x16AF4],
|
||||||
|
[0x16B30, 0x16B36], [0x16F4F, 0x16F4F], [0x16F8F, 0x16F92],
|
||||||
|
[0x1BC9D, 0x1BC9E], [0x1BCA0, 0x1BCA3], [0x1D167, 0x1D169],
|
||||||
|
[0x1D173, 0x1D182], [0x1D185, 0x1D18B], [0x1D1AA, 0x1D1AD],
|
||||||
|
[0x1D242, 0x1D244], [0x1DA00, 0x1DA36], [0x1DA3B, 0x1DA6C],
|
||||||
|
[0x1DA75, 0x1DA75], [0x1DA84, 0x1DA84], [0x1DA9B, 0x1DA9F],
|
||||||
|
[0x1DAA1, 0x1DAAF], [0x1E000, 0x1E006], [0x1E008, 0x1E018],
|
||||||
|
[0x1E01B, 0x1E021], [0x1E023, 0x1E024], [0x1E026, 0x1E02A],
|
||||||
|
[0x1E130, 0x1E136], [0x1E2EC, 0x1E2EF], [0x1E8D0, 0x1E8D6],
|
||||||
|
[0x1E944, 0x1E94A], [0xE0001, 0xE0001], [0xE0020, 0xE007F],
|
||||||
|
[0xE0100, 0xE01EF]
|
||||||
|
];
|
||||||
|
const BMP_WIDE = [
|
||||||
|
[0x1100, 0x115F], [0x231A, 0x231B], [0x2329, 0x232A],
|
||||||
|
[0x23E9, 0x23EC], [0x23F0, 0x23F0], [0x23F3, 0x23F3],
|
||||||
|
[0x25FD, 0x25FE], [0x2614, 0x2615], [0x2648, 0x2653],
|
||||||
|
[0x267F, 0x267F], [0x2693, 0x2693], [0x26A1, 0x26A1],
|
||||||
|
[0x26AA, 0x26AB], [0x26BD, 0x26BE], [0x26C4, 0x26C5],
|
||||||
|
[0x26CE, 0x26CE], [0x26D4, 0x26D4], [0x26EA, 0x26EA],
|
||||||
|
[0x26F2, 0x26F3], [0x26F5, 0x26F5], [0x26FA, 0x26FA],
|
||||||
|
[0x26FD, 0x26FD], [0x2705, 0x2705], [0x270A, 0x270B],
|
||||||
|
[0x2728, 0x2728], [0x274C, 0x274C], [0x274E, 0x274E],
|
||||||
|
[0x2753, 0x2755], [0x2757, 0x2757], [0x2795, 0x2797],
|
||||||
|
[0x27B0, 0x27B0], [0x27BF, 0x27BF], [0x2B1B, 0x2B1C],
|
||||||
|
[0x2B50, 0x2B50], [0x2B55, 0x2B55], [0x2E80, 0x2E99],
|
||||||
|
[0x2E9B, 0x2EF3], [0x2F00, 0x2FD5], [0x2FF0, 0x2FFB],
|
||||||
|
[0x3000, 0x3029], [0x302E, 0x303E], [0x3041, 0x3096],
|
||||||
|
[0x309B, 0x30FF], [0x3105, 0x312F], [0x3131, 0x318E],
|
||||||
|
[0x3190, 0x31BA], [0x31C0, 0x31E3], [0x31F0, 0x321E],
|
||||||
|
[0x3220, 0x3247], [0x3250, 0x4DBF], [0x4E00, 0xA48C],
|
||||||
|
[0xA490, 0xA4C6], [0xA960, 0xA97C], [0xAC00, 0xD7A3],
|
||||||
|
[0xF900, 0xFAFF], [0xFE10, 0xFE19], [0xFE30, 0xFE52],
|
||||||
|
[0xFE54, 0xFE66], [0xFE68, 0xFE6B], [0xFF01, 0xFF60],
|
||||||
|
[0xFFE0, 0xFFE6]
|
||||||
|
];
|
||||||
|
const HIGH_WIDE = [
|
||||||
|
[0x16FE0, 0x16FE3], [0x17000, 0x187F7],
|
||||||
|
[0x18800, 0x18AF2], [0x1B000, 0x1B11E], [0x1B150, 0x1B152],
|
||||||
|
[0x1B164, 0x1B167], [0x1B170, 0x1B2FB], [0x1F004, 0x1F004],
|
||||||
|
[0x1F0CF, 0x1F0CF], [0x1F18E, 0x1F18E], [0x1F191, 0x1F19A],
|
||||||
|
[0x1F200, 0x1F202], [0x1F210, 0x1F23B], [0x1F240, 0x1F248],
|
||||||
|
[0x1F250, 0x1F251], [0x1F260, 0x1F265], [0x1F300, 0x1F320],
|
||||||
|
[0x1F32D, 0x1F335], [0x1F337, 0x1F37C], [0x1F37E, 0x1F393],
|
||||||
|
[0x1F3A0, 0x1F3CA], [0x1F3CF, 0x1F3D3], [0x1F3E0, 0x1F3F0],
|
||||||
|
[0x1F3F4, 0x1F3F4], [0x1F3F8, 0x1F43E], [0x1F440, 0x1F440],
|
||||||
|
[0x1F442, 0x1F4FC], [0x1F4FF, 0x1F53D], [0x1F54B, 0x1F54E],
|
||||||
|
[0x1F550, 0x1F567], [0x1F57A, 0x1F57A], [0x1F595, 0x1F596],
|
||||||
|
[0x1F5A4, 0x1F5A4], [0x1F5FB, 0x1F64F], [0x1F680, 0x1F6C5],
|
||||||
|
[0x1F6CC, 0x1F6CC], [0x1F6D0, 0x1F6D2], [0x1F6D5, 0x1F6D5],
|
||||||
|
[0x1F6EB, 0x1F6EC], [0x1F6F4, 0x1F6FA], [0x1F7E0, 0x1F7EB],
|
||||||
|
[0x1F90D, 0x1F971], [0x1F973, 0x1F976], [0x1F97A, 0x1F9A2],
|
||||||
|
[0x1F9A5, 0x1F9AA], [0x1F9AE, 0x1F9CA], [0x1F9CD, 0x1F9FF],
|
||||||
|
[0x1FA70, 0x1FA73], [0x1FA78, 0x1FA7A], [0x1FA80, 0x1FA82],
|
||||||
|
[0x1FA90, 0x1FA95], [0x20000, 0x2FFFD], [0x30000, 0x3FFFD]
|
||||||
|
];
|
||||||
|
|
||||||
|
// BMP lookup table, lazy initialized during first addon loading
|
||||||
|
let table: Uint8Array;
|
||||||
|
|
||||||
|
function bisearch(ucs: number, data: number[][]): boolean {
|
||||||
|
let min = 0;
|
||||||
|
let max = data.length - 1;
|
||||||
|
let mid;
|
||||||
|
if (ucs < data[0][0] || ucs > data[max][1]) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
while (max >= min) {
|
||||||
|
mid = (min + max) >> 1;
|
||||||
|
if (ucs > data[mid][1]) {
|
||||||
|
min = mid + 1;
|
||||||
|
} else if (ucs < data[mid][0]) {
|
||||||
|
max = mid - 1;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class UnicodeV11 implements IUnicodeVersionProvider {
|
||||||
|
public readonly version = '11';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
if (!table) {
|
||||||
|
table = new Uint8Array(65536);
|
||||||
|
fill(table, 1);
|
||||||
|
table[0] = 0;
|
||||||
|
fill(table, 0, 1, 32);
|
||||||
|
fill(table, 0, 0x7f, 0xa0);
|
||||||
|
for (let r = 0; r < BMP_COMBINING.length; ++r) {
|
||||||
|
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
|
||||||
|
}
|
||||||
|
for (let r = 0; r < BMP_WIDE.length; ++r) {
|
||||||
|
fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public wcwidth(num: number): CharWidth {
|
||||||
|
if (num < 32) return 0;
|
||||||
|
if (num < 127) return 1;
|
||||||
|
if (num < 65536) return table[num] as CharWidth;
|
||||||
|
if (bisearch(num, HIGH_COMBINING)) return 0;
|
||||||
|
if (bisearch(num, HIGH_WIDE)) return 2;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2015",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"common/*": [
|
||||||
|
"../../../src/common/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../src/common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { assert } from 'chai';
|
||||||
|
import { openTerminal, getBrowserType } from '../../../out-test/api/TestUtils';
|
||||||
|
import { Browser, Page } from 'playwright';
|
||||||
|
|
||||||
|
const APP = 'http://127.0.0.1:3001/test';
|
||||||
|
|
||||||
|
let browser: Browser;
|
||||||
|
let page: Page;
|
||||||
|
const width = 800;
|
||||||
|
const height = 600;
|
||||||
|
|
||||||
|
describe('Unicode11Addon', () => {
|
||||||
|
before(async function(): Promise<any> {
|
||||||
|
const browserType = getBrowserType();
|
||||||
|
browser = await browserType.launch({
|
||||||
|
headless: process.argv.includes('--headless')
|
||||||
|
});
|
||||||
|
page = await (await browser.newContext()).newPage();
|
||||||
|
await page.setViewportSize({ width, height });
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async function(): Promise<any> {
|
||||||
|
await page.goto(APP);
|
||||||
|
await openTerminal(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('wcwidth V11 emoji test', async () => {
|
||||||
|
await page.evaluate(`
|
||||||
|
window.unicode11 = new Unicode11Addon();
|
||||||
|
window.term.loadAddon(window.unicode11);
|
||||||
|
`);
|
||||||
|
// should have loaded '11'
|
||||||
|
assert.deepEqual(await page.evaluate(`window.term.unicode.versions`), ['6', '11']);
|
||||||
|
// switch should not throw
|
||||||
|
await page.evaluate(`window.term.unicode.activeVersion = '11';`);
|
||||||
|
assert.deepEqual(await page.evaluate(`window.term.unicode.activeVersion`), '11');
|
||||||
|
// v6: 10, V11: 20
|
||||||
|
assert.deepEqual(await page.evaluate(`window.term._core.unicodeService.getStringCellWidth('🤣🤣🤣🤣🤣🤣🤣🤣🤣🤣')`), 20);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2015",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"common/*": [
|
||||||
|
"../../../src/common/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../src/common"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Terminal, ITerminalAddon } from 'xterm';
|
||||||
|
|
||||||
|
declare module 'xterm-addon-unicode11' {
|
||||||
|
export class Unicode11Addon implements ITerminalAddon {
|
||||||
|
constructor();
|
||||||
|
public activate(terminal: Terminal): void;
|
||||||
|
public dispose(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const addonName = 'Unicode11Addon';
|
||||||
|
const mainFile = 'xterm-addon-unicode11.js';
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: `./out/${addonName}.js`,
|
||||||
|
devtool: 'source-map',
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.js$/,
|
||||||
|
use: ["source-map-loader"],
|
||||||
|
enforce: "pre",
|
||||||
|
exclude: /node_modules/
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
modules: ['./node_modules'],
|
||||||
|
extensions: [ '.js' ],
|
||||||
|
alias: {
|
||||||
|
common: path.resolve('../../out/common')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
filename: mainFile,
|
||||||
|
path: path.resolve('./lib'),
|
||||||
|
library: addonName,
|
||||||
|
libraryTarget: 'umd'
|
||||||
|
},
|
||||||
|
mode: 'production'
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-web-links",
|
"name": "xterm-addon-web-links",
|
||||||
"version": "0.2.1",
|
"version": "0.4.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../../node_modules/.bin/tsc -p src",
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 _options: ILinkProviderOptions = {}
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void {
|
||||||
|
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, 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);
|
||||||
|
|
||||||
|
let match;
|
||||||
|
let stringIndex = -1;
|
||||||
|
const result: ILink[] = [];
|
||||||
|
|
||||||
|
while ((match = rex.exec(line)) !== null) {
|
||||||
|
const text = match[1];
|
||||||
|
if (!text) {
|
||||||
|
// something matched but does not comply with the given matchIndex
|
||||||
|
// since this is most likely a bug the regex itself we simply do nothing here
|
||||||
|
console.log('match found without corresponding matchIndex');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get index, match.index is for the outer match which includes negated chars
|
||||||
|
// therefore we cannot use match.index directly, instead we search the position
|
||||||
|
// of the match group in text again
|
||||||
|
// also correct regex and string search offsets for the next loop run
|
||||||
|
stringIndex = line.indexOf(text, stringIndex + 1);
|
||||||
|
rex.lastIndex = stringIndex + text.length;
|
||||||
|
if (stringIndex < 0) {
|
||||||
|
// invalid stringIndex (should not have happened)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
let endX = stringIndex + text.length;
|
||||||
|
let endY = startLineIndex + 1;
|
||||||
|
|
||||||
|
while (endX > terminal.cols) {
|
||||||
|
endX -= terminal.cols;
|
||||||
|
endY++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
start: {
|
||||||
|
x: stringIndex + 1,
|
||||||
|
y: startLineIndex + 1
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
x: endX,
|
||||||
|
y: endY
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push({ range, text, activate });
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the entire line for the buffer line
|
||||||
|
* @param line The line being translated.
|
||||||
|
* @param trimRight Whether to trim whitespace to the right.
|
||||||
|
* @param terminal The terminal
|
||||||
|
*/
|
||||||
|
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
|
||||||
|
let lineString = '';
|
||||||
|
let lineWrapsToNext: boolean;
|
||||||
|
let prevLinesToWrap: boolean;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const line = terminal.buffer.active.getLine(lineIndex);
|
||||||
|
if (!line) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.isWrapped) {
|
||||||
|
lineIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
prevLinesToWrap = line.isWrapped;
|
||||||
|
} while (prevLinesToWrap);
|
||||||
|
|
||||||
|
const startLineIndex = lineIndex;
|
||||||
|
|
||||||
|
do {
|
||||||
|
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
|
||||||
|
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||||
|
const line = terminal.buffer.active.getLine(lineIndex);
|
||||||
|
if (!line) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
|
||||||
|
lineIndex++;
|
||||||
|
} while (lineWrapsToNext);
|
||||||
|
|
||||||
|
return [lineString, startLineIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable, IViewportRange } from 'xterm';
|
||||||
|
import { WebLinkProvider } from './WebLinkProvider';
|
||||||
|
|
||||||
const protocolClause = '(https?:\\/\\/)';
|
const protocolClause = '(https?:\\/\\/)';
|
||||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||||
@@ -14,7 +15,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
|||||||
const localHostClause = '(localhost)';
|
const localHostClause = '(localhost)';
|
||||||
const portClause = '(:\\d{1,5})';
|
const portClause = '(:\\d{1,5})';
|
||||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||||
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])';
|
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+@]*)*([^:"\'\\s])';
|
||||||
const pathClause = '(' + pathCharacterSet + ')?';
|
const pathClause = '(' + pathCharacterSet + ')?';
|
||||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||||
@@ -26,28 +27,51 @@ const end = ')($|' + negatedPathCharacterSet + ')';
|
|||||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||||
|
|
||||||
function handleLink(event: MouseEvent, uri: string): void {
|
function handleLink(event: MouseEvent, uri: string): void {
|
||||||
window.open(uri, '_blank');
|
const newWindow = window.open();
|
||||||
|
if (newWindow) {
|
||||||
|
newWindow.opener = null;
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class WebLinksAddon implements ITerminalAddon {
|
export class WebLinksAddon implements ITerminalAddon {
|
||||||
private _linkMatcherId: number | undefined;
|
private _linkMatcherId: number | undefined;
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
|
private _linkProvider: IDisposable | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
|
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 {
|
public activate(terminal: Terminal): void {
|
||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, this._handler, this._options);
|
|
||||||
|
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
|
||||||
|
const options = this._options as ILinkProviderOptions;
|
||||||
|
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, strictUrlRegex, this._handler, options));
|
||||||
|
} else {
|
||||||
|
// TODO: This should be removed eventually
|
||||||
|
const options = this._options as ILinkMatcherOptions;
|
||||||
|
options.matchIndex = 1;
|
||||||
|
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (this._linkMatcherId !== undefined && this._terminal !== undefined) {
|
if (this._linkMatcherId !== undefined && this._terminal !== undefined) {
|
||||||
this._terminal.deregisterLinkMatcher(this._linkMatcherId);
|
this._terminal.deregisterLinkMatcher(this._linkMatcherId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._linkProvider?.dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@
|
|||||||
"outDir": "../out",
|
"outDir": "../out",
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
|
|||||||
+13
-53
@@ -3,55 +3,45 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as puppeteer from 'puppeteer';
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { ITerminalOptions } from 'xterm';
|
import { openTerminal, pollFor, writeSync, getBrowserType } 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: puppeteer.Browser;
|
let browser: Browser;
|
||||||
let page: puppeteer.Page;
|
let page: Page;
|
||||||
const width = 800;
|
const width = 800;
|
||||||
const height = 600;
|
const height = 600;
|
||||||
|
|
||||||
describe('WebLinksAddon', () => {
|
describe('WebLinksAddon', () => {
|
||||||
before(async function(): Promise<any> {
|
before(async function(): Promise<any> {
|
||||||
this.timeout(10000);
|
const browserType = getBrowserType();
|
||||||
browser = await puppeteer.launch({
|
browser = await browserType.launch({
|
||||||
headless: process.argv.indexOf('--headless') !== -1,
|
headless: process.argv.includes('--headless')
|
||||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
|
||||||
});
|
});
|
||||||
page = (await browser.pages())[0];
|
page = await (await browser.newContext()).newPage();
|
||||||
await page.setViewport({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
});
|
});
|
||||||
|
|
||||||
after(async () => {
|
after(async () => await browser.close());
|
||||||
await browser.close();
|
beforeEach(async () => await page.goto(APP));
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(async function(): Promise<any> {
|
|
||||||
this.timeout(5000);
|
|
||||||
await page.goto(APP);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('.com', async function(): Promise<any> {
|
it('.com', async function(): Promise<any> {
|
||||||
this.timeout(20000);
|
|
||||||
await testHostName('foo.com');
|
await testHostName('foo.com');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('.com.au', async function(): Promise<any> {
|
it('.com.au', async function(): Promise<any> {
|
||||||
this.timeout(20000);
|
|
||||||
await testHostName('foo.com.au');
|
await testHostName('foo.com.au');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('.io', async function(): Promise<any> {
|
it('.io', async function(): Promise<any> {
|
||||||
this.timeout(20000);
|
|
||||||
await testHostName('foo.io');
|
await testHostName('foo.io');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function testHostName(hostname: string): Promise<void> {
|
async function testHostName(hostname: string): Promise<void> {
|
||||||
await openTerminal({ rendererType: 'dom' });
|
await openTerminal(page, { rendererType: 'dom', cols: 40 });
|
||||||
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
|
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
|
||||||
const data = ` http://${hostname} \\r\\n` +
|
const data = ` http://${hostname} \\r\\n` +
|
||||||
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
|
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
|
||||||
@@ -70,16 +60,6 @@ async function testHostName(hostname: string): Promise<void> {
|
|||||||
await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
|
await pollForLinkAtCell(1, 7, `http://${hostname}/subpath/+/id`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
|
||||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
|
||||||
if (options.rendererType === 'dom') {
|
|
||||||
await page.waitForSelector('.xterm-rows');
|
|
||||||
} else {
|
|
||||||
await page.waitForSelector('.xterm-text-layer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
|
async function pollForLinkAtCell(col: number, row: number, value: string): Promise<void> {
|
||||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||||
// Ensure the hover element exists before trying to hover it
|
// Ensure the hover element exists before trying to hover it
|
||||||
@@ -87,23 +67,3 @@ async function pollForLinkAtCell(col: number, row: number, value: string): Promi
|
|||||||
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
|
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
|
||||||
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
|
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function pollFor(page: puppeteer.Page, fn: string, val: any, preFn?: () => Promise<void>): Promise<void> {
|
|
||||||
if (preFn) {
|
|
||||||
await preFn();
|
|
||||||
}
|
|
||||||
const result = await page.evaluate(fn);
|
|
||||||
if (result !== val) {
|
|
||||||
return new Promise<void>(r => {
|
|
||||||
setTimeout(() => r(pollFor(page, fn, val, preFn)), 10);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeSync(page: puppeteer.Page, data: string): Promise<void> {
|
|
||||||
await page.evaluate(`
|
|
||||||
window.ready = false;
|
|
||||||
window.term.write('${data}', () => window.ready = true);
|
|
||||||
`);
|
|
||||||
await pollFor(page, 'window.ready', true);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es5",
|
||||||
|
"lib": [
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out-test",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha",
|
||||||
|
"../../../node_modules/@types/node",
|
||||||
|
"../../../out-test/api/TestUtils"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./src" },
|
||||||
|
{ "path": "./test" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm';
|
||||||
|
|
||||||
declare module 'xterm-addon-web-links' {
|
declare module 'xterm-addon-web-links' {
|
||||||
/**
|
/**
|
||||||
@@ -15,8 +15,12 @@ declare module 'xterm-addon-web-links' {
|
|||||||
* Creates a new web links addon.
|
* Creates a new web links addon.
|
||||||
* @param handler The callback when the link is called.
|
* @param handler The callback when the link is called.
|
||||||
* @param options Options for the link matcher.
|
* @param options Options for the link matcher.
|
||||||
|
* @param useLinkProvider Whether to use the new link provider API to create
|
||||||
|
* the links. This is an option because use of both link matcher (old) and
|
||||||
|
* 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);
|
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates the addon
|
* Activates the addon
|
||||||
@@ -29,4 +33,21 @@ declare module 'xterm-addon-web-links' {
|
|||||||
*/
|
*/
|
||||||
public dispose(): void;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
tsconfig.json
|
tsconfig.json
|
||||||
.yarnrc
|
.yarnrc
|
||||||
webpack.config.js
|
webpack.config.js
|
||||||
|
out-test/
|
||||||
|
test/
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
## xterm-addon-webgl
|
## 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+.
|
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a WebGL2-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) ⚠️
|
|
||||||
|
|
||||||
### Install
|
### 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.
|
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",
|
"name": "xterm-addon-webgl",
|
||||||
"version": "0.4.0",
|
"version": "0.11.1",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "../../node_modules/.bin/tsc -p src",
|
"build": "../../node_modules/.bin/tsc -p .",
|
||||||
"prepackage": "npm run build",
|
"prepackage": "npm run build",
|
||||||
"package": "../../node_modules/.bin/webpack",
|
"package": "../../node_modules/.bin/webpack",
|
||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||||
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET } from './RenderModel';
|
import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_INDICIES_PER_CELL, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_BG_OFFSET } from './RenderModel';
|
||||||
import { fill } from 'common/TypedArrayUtils';
|
import { fill } from 'common/TypedArrayUtils';
|
||||||
import { slice } from './TypedArray';
|
import { slice } from './TypedArray';
|
||||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
|
import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, Attributes, FgFlags } from 'common/buffer/Constants';
|
||||||
import { Terminal, IBufferLine } from 'xterm';
|
import { Terminal, IBufferLine } from 'xterm';
|
||||||
import { IColorSet } from 'browser/Types';
|
import { IColorSet, IColor } from 'browser/Types';
|
||||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||||
|
import { AttributeData } from 'common/buffer/AttributeData';
|
||||||
|
|
||||||
interface IVertices {
|
interface IVertices {
|
||||||
attributes: Float32Array;
|
attributes: Float32Array;
|
||||||
@@ -212,14 +213,14 @@ export class GlyphRenderer {
|
|||||||
// a_cellpos only changes on resize
|
// a_cellpos only changes on resize
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSelection(model: IRenderModel, columnSelectMode: boolean): void {
|
public updateSelection(model: IRenderModel): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
|
|
||||||
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
this._vertices.selectionAttributes = slice(this._vertices.attributes, 0);
|
||||||
|
|
||||||
const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
|
const bg = (this._colors.selectionOpaque.rgba >>> 8) | Attributes.CM_RGB;
|
||||||
|
|
||||||
if (columnSelectMode) {
|
if (model.selection.columnSelectMode) {
|
||||||
const startCol = model.selection.startCol;
|
const startCol = model.selection.startCol;
|
||||||
const width = model.selection.endCol - startCol;
|
const width = model.selection.endCol - startCol;
|
||||||
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
const height = model.selection.viewportCappedEndRow - model.selection.viewportCappedStartRow + 1;
|
||||||
@@ -249,23 +250,54 @@ export class GlyphRenderer {
|
|||||||
|
|
||||||
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
|
private _updateSelectionRange(startCol: number, endCol: number, y: number, model: IRenderModel, bg: number): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
const row = y + terminal.buffer.viewportY;
|
const row = y + terminal.buffer.active.viewportY;
|
||||||
let line: IBufferLine | undefined;
|
let line: IBufferLine | undefined;
|
||||||
for (let x = startCol; x < endCol; x++) {
|
for (let x = startCol; x < endCol; x++) {
|
||||||
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
const offset = (y * this._terminal.cols + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||||
const code = model.cells[offset];
|
const code = model.cells[offset];
|
||||||
|
let fg = model.cells[offset + RENDER_MODEL_FG_OFFSET];
|
||||||
|
if (fg & FgFlags.INVERSE) {
|
||||||
|
const workCell = new AttributeData();
|
||||||
|
workCell.fg = fg;
|
||||||
|
workCell.bg = model.cells[offset + RENDER_MODEL_BG_OFFSET];
|
||||||
|
// Get attributes from fg (excluding inverse) and resolve inverse by pullibng rgb colors
|
||||||
|
// from bg. This is needed since the inverse fg color should be based on the original bg
|
||||||
|
// color, not on the selection color
|
||||||
|
fg &= ~(Attributes.CM_MASK | Attributes.RGB_MASK | FgFlags.INVERSE);
|
||||||
|
switch (workCell.getBgColorMode()) {
|
||||||
|
case Attributes.CM_P16:
|
||||||
|
case Attributes.CM_P256:
|
||||||
|
const c = this._getColorFromAnsiIndex(workCell.getBgColor()).rgba;
|
||||||
|
fg |= (c >> 8) & Attributes.RED_MASK | (c >> 8) & Attributes.GREEN_MASK | (c >> 8) & Attributes.BLUE_MASK;
|
||||||
|
case Attributes.CM_RGB:
|
||||||
|
const arr = AttributeData.toColorRGB(workCell.getBgColor());
|
||||||
|
fg |= arr[0] << Attributes.RED_SHIFT | arr[1] << Attributes.GREEN_SHIFT | arr[2] << Attributes.BLUE_SHIFT;
|
||||||
|
case Attributes.CM_DEFAULT:
|
||||||
|
default:
|
||||||
|
const c2 = this._colors.background.rgba;
|
||||||
|
fg |= (c2 >> 8) & Attributes.RED_MASK | (c2 >> 8) & Attributes.GREEN_MASK | (c2 >> 8) & Attributes.BLUE_MASK;
|
||||||
|
}
|
||||||
|
fg |= Attributes.CM_RGB;
|
||||||
|
}
|
||||||
if (code & COMBINED_CHAR_BIT_MASK) {
|
if (code & COMBINED_CHAR_BIT_MASK) {
|
||||||
if (!line) {
|
if (!line) {
|
||||||
line = terminal.buffer.getLine(row);
|
line = terminal.buffer.active.getLine(row);
|
||||||
}
|
}
|
||||||
const chars = line!.getCell(x)!.char;
|
const chars = line!.getCell(x)!.getChars();
|
||||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET], chars);
|
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg, chars);
|
||||||
} else {
|
} else {
|
||||||
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, model.cells[offset + RENDER_MODEL_FG_OFFSET]);
|
this._updateCell(this._vertices.selectionAttributes, x, y, model.cells[offset], bg, fg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||||
|
if (idx >= this._colors.ansi.length) {
|
||||||
|
throw new Error('No color found for idx ' + idx);
|
||||||
|
}
|
||||||
|
return this._colors.ansi[idx];
|
||||||
|
}
|
||||||
|
|
||||||
public onResize(): void {
|
public onResize(): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
const gl = this._gl;
|
const gl = this._gl;
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ export class RectangleRenderer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSelection(model: ISelectionRenderModel, columnSelectMode: boolean): void {
|
public updateSelection(model: ISelectionRenderModel): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
|
|
||||||
if (!model.hasSelection) {
|
if (!model.hasSelection) {
|
||||||
@@ -179,7 +179,7 @@ export class RectangleRenderer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (columnSelectMode) {
|
if (model.columnSelectMode) {
|
||||||
const startCol = model.startCol;
|
const startCol = model.startCol;
|
||||||
const width = model.endCol - startCol;
|
const width = model.endCol - startCol;
|
||||||
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
const height = model.viewportCappedEndRow - model.viewportCappedStartRow + 1;
|
||||||
@@ -196,7 +196,7 @@ export class RectangleRenderer {
|
|||||||
} else {
|
} else {
|
||||||
// Draw first row
|
// Draw first row
|
||||||
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
const startCol = model.viewportStartRow === model.viewportCappedStartRow ? model.startCol : 0;
|
||||||
const startRowEndCol = model.viewportCappedStartRow === model.viewportCappedEndRow ? model.endCol : terminal.cols;
|
const startRowEndCol = model.viewportCappedStartRow === model.viewportEndRow ? model.endCol : terminal.cols;
|
||||||
this._addRectangleFloat(
|
this._addRectangleFloat(
|
||||||
this._vertices.selection,
|
this._vertices.selection,
|
||||||
0,
|
0,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export class RenderModel implements IRenderModel {
|
|||||||
this.lineLengths = new Uint32Array(0);
|
this.lineLengths = new Uint32Array(0);
|
||||||
this.selection = {
|
this.selection = {
|
||||||
hasSelection: false,
|
hasSelection: false,
|
||||||
|
columnSelectMode: false,
|
||||||
viewportStartRow: 0,
|
viewportStartRow: 0,
|
||||||
viewportEndRow: 0,
|
viewportEndRow: 0,
|
||||||
viewportCappedStartRow: 0,
|
viewportCappedStartRow: 0,
|
||||||
@@ -42,7 +43,6 @@ export class RenderModel implements IRenderModel {
|
|||||||
public clear(): void {
|
public clear(): void {
|
||||||
fill(this.cells, 0, 0);
|
fill(this.cells, 0, 0);
|
||||||
fill(this.lineLengths, 0, 0);
|
fill(this.lineLengths, 0, 0);
|
||||||
this.clearSelection();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearSelection(): void {
|
public clearSelection(): void {
|
||||||
|
|||||||
@@ -5,9 +5,7 @@
|
|||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { sliceFallback } from './TypedArray';
|
import { sliceFallback } from './TypedArray';
|
||||||
|
|
||||||
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
|
||||||
| Int8Array | Int16Array | Int32Array
|
|
||||||
| Float32Array | Float64Array;
|
|
||||||
|
|
||||||
function deepEquals(a: TypedArray, b: TypedArray): void {
|
function deepEquals(a: TypedArray, b: TypedArray): void {
|
||||||
assert.equal(a.length, b.length);
|
assert.equal(a.length, b.length);
|
||||||
@@ -40,19 +38,19 @@ describe('polyfill conformance tests', function(): void {
|
|||||||
deepEquals(sliceFallback(u321, -1), u322.slice(-1));
|
deepEquals(sliceFallback(u321, -1), u322.slice(-1));
|
||||||
});
|
});
|
||||||
it('Int8Array', () => {
|
it('Int8Array', () => {
|
||||||
const i81 = new Int8Array(5);
|
const i81 = new Int8Array(5);
|
||||||
const i82 = new Int8Array(5);
|
const i82 = new Int8Array(5);
|
||||||
deepEquals(sliceFallback(i81, 2), i82.slice(2));
|
deepEquals(sliceFallback(i81, 2), i82.slice(2));
|
||||||
deepEquals(sliceFallback(i81, 65537), i82.slice(65537));
|
deepEquals(sliceFallback(i81, 65537), i82.slice(65537));
|
||||||
deepEquals(sliceFallback(i81, -1), i82.slice(-1));
|
deepEquals(sliceFallback(i81, -1), i82.slice(-1));
|
||||||
});
|
});
|
||||||
it('Int16Array', () => {
|
it('Int16Array', () => {
|
||||||
const i161 = new Int16Array(5);
|
const i161 = new Int16Array(5);
|
||||||
const i162 = new Int16Array(5);
|
const i162 = new Int16Array(5);
|
||||||
deepEquals(sliceFallback(i161, 2), i162.slice(2));
|
deepEquals(sliceFallback(i161, 2), i162.slice(2));
|
||||||
deepEquals(sliceFallback(i161, 65535), i162.slice(65535));
|
deepEquals(sliceFallback(i161, 65535), i162.slice(65535));
|
||||||
deepEquals(sliceFallback(i161, -1), i162.slice(-1));
|
deepEquals(sliceFallback(i161, -1), i162.slice(-1));
|
||||||
});
|
});
|
||||||
it('Int32Array', () => {
|
it('Int32Array', () => {
|
||||||
const i321 = new Int32Array(5);
|
const i321 = new Int32Array(5);
|
||||||
const i322 = new Int32Array(5);
|
const i322 = new Int32Array(5);
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray
|
export type TypedArray = Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Int8Array | Int16Array | Int32Array | Float32Array | Float64Array;
|
||||||
| Int8Array | Int16Array | Int32Array
|
|
||||||
| Float32Array | Float64Array;
|
|
||||||
|
|
||||||
export function slice<T extends TypedArray>(array: T, start?: number, end?: number): T {
|
export function slice<T extends TypedArray>(array: T, start?: number, end?: number): T {
|
||||||
// all modern engines that support .slice
|
// all modern engines that support .slice
|
||||||
|
|||||||
+1
@@ -56,6 +56,7 @@ export interface IRenderModel {
|
|||||||
|
|
||||||
export interface ISelectionRenderModel {
|
export interface ISelectionRenderModel {
|
||||||
hasSelection: boolean;
|
hasSelection: boolean;
|
||||||
|
columnSelectMode: boolean;
|
||||||
viewportStartRow: number;
|
viewportStartRow: number;
|
||||||
viewportEndRow: number;
|
viewportEndRow: number;
|
||||||
viewportCappedStartRow: number;
|
viewportCappedStartRow: number;
|
||||||
|
|||||||
@@ -3,13 +3,17 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ITerminalAddon } from 'xterm';
|
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||||
import { WebglRenderer } from './WebglRenderer';
|
import { WebglRenderer } from './WebglRenderer';
|
||||||
import { IRenderService } from 'browser/services/Services';
|
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
|
||||||
import { IColorSet } from 'browser/Types';
|
import { IColorSet } from 'browser/Types';
|
||||||
|
import { EventEmitter } from 'common/EventEmitter';
|
||||||
|
|
||||||
export class WebglAddon implements ITerminalAddon {
|
export class WebglAddon implements ITerminalAddon {
|
||||||
private _terminal?: Terminal;
|
private _terminal?: Terminal;
|
||||||
|
private _renderer?: WebglRenderer;
|
||||||
|
private _onContextLoss = new EventEmitter<void>();
|
||||||
|
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _preserveDrawingBuffer?: boolean
|
private _preserveDrawingBuffer?: boolean
|
||||||
@@ -20,17 +24,29 @@ export class WebglAddon implements ITerminalAddon {
|
|||||||
throw new Error('Cannot activate WebglAddon before Terminal.open');
|
throw new Error('Cannot activate WebglAddon before Terminal.open');
|
||||||
}
|
}
|
||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
const renderService: IRenderService = (<any>terminal)._core._renderService;
|
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||||
const colors: IColorSet = (<any>terminal)._core._colorManager.colors;
|
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||||
renderService.setRenderer(new WebglRenderer(terminal, colors, this._preserveDrawingBuffer));
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
if (!this._terminal) {
|
if (!this._terminal) {
|
||||||
throw new Error('Cannot dispose WebglAddon because it is activated');
|
throw new Error('Cannot dispose WebglAddon because it is activated');
|
||||||
}
|
}
|
||||||
const renderService: IRenderService = (<any>this._terminal)._core._renderService;
|
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||||
renderService.setRenderer((<any>this._terminal)._core._createRenderer());
|
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||||
renderService.onResize(this._terminal.cols, this._terminal.rows);
|
renderService.onResize(this._terminal.cols, this._terminal.rows);
|
||||||
|
this._renderer = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||||
|
return this._renderer?.textureAtlas;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearTextureAtlas(): void {
|
||||||
|
this._renderer?.clearCharAtlas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ITerminal } from '../../../src/Types';
|
|
||||||
import { GlyphRenderer } from './GlyphRenderer';
|
import { GlyphRenderer } from './GlyphRenderer';
|
||||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||||
@@ -13,13 +12,17 @@ import { RectangleRenderer } from './RectangleRenderer';
|
|||||||
import { IWebGL2RenderingContext } from './Types';
|
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 { 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 { 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 { Terminal, IEvent } from 'xterm';
|
||||||
import { IRenderLayer } from './renderLayer/Types';
|
import { IRenderLayer } from './renderLayer/Types';
|
||||||
import { IRenderDimensions, IRenderer, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
|
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||||
import { IColorSet } from 'browser/Types';
|
import { ITerminal, IColorSet } from 'browser/Types';
|
||||||
import { EventEmitter } from 'common/EventEmitter';
|
import { EventEmitter } from 'common/EventEmitter';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
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 {
|
export class WebglRenderer extends Disposable implements IRenderer {
|
||||||
private _renderLayers: IRenderLayer[];
|
private _renderLayers: IRenderLayer[];
|
||||||
@@ -39,21 +42,25 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
private _core: ITerminal;
|
private _core: ITerminal;
|
||||||
private _isAttached: boolean;
|
private _isAttached: boolean;
|
||||||
|
|
||||||
private _onRequestRefreshRows = new EventEmitter<IRequestRefreshRowsEvent>();
|
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||||
public get onRequestRefreshRows(): IEvent<IRequestRefreshRowsEvent> { return this._onRequestRefreshRows.event; }
|
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||||
|
|
||||||
|
private _onContextLoss = new EventEmitter<void>();
|
||||||
|
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private _terminal: Terminal,
|
private _terminal: Terminal,
|
||||||
private _colors: IColorSet,
|
private _colors: IColorSet,
|
||||||
|
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||||
preserveDrawingBuffer?: boolean
|
preserveDrawingBuffer?: boolean
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._core = (<any>this._terminal)._core;
|
this._core = (this._terminal as any)._core;
|
||||||
|
|
||||||
this._renderLayers = [
|
this._renderLayers = [
|
||||||
new LinkRenderLayer(this._core.screenElement, 2, this._colors, this._core),
|
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
|
||||||
new CursorRenderLayer(this._core.screenElement, 3, this._colors, this._onRequestRefreshRows)
|
new CursorRenderLayer(this._core.screenElement!, 3, this._colors, this._onRequestRedraw)
|
||||||
];
|
];
|
||||||
this.dimensions = {
|
this.dimensions = {
|
||||||
scaledCharWidth: 0,
|
scaledCharWidth: 0,
|
||||||
@@ -81,9 +88,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
};
|
};
|
||||||
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
this._gl = this._canvas.getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
||||||
if (!this._gl) {
|
if (!this._gl) {
|
||||||
throw new Error('WebGL2 not supported');
|
throw new Error('WebGL2 not supported ' + this._gl);
|
||||||
}
|
}
|
||||||
this._core.screenElement.appendChild(this._canvas);
|
|
||||||
|
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);
|
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||||
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||||
@@ -91,22 +101,28 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// Update dimensions and acquire char atlas
|
// Update dimensions and acquire char atlas
|
||||||
this.onCharSizeChanged();
|
this.onCharSizeChanged();
|
||||||
|
|
||||||
this._isAttached = document.body.contains(this._core.screenElement);
|
this._isAttached = document.body.contains(this._core.screenElement!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this._renderLayers.forEach(l => l.dispose());
|
for (const l of this._renderLayers) {
|
||||||
this._core.screenElement.removeChild(this._canvas);
|
l.dispose();
|
||||||
|
}
|
||||||
|
this._core.screenElement!.removeChild(this._canvas);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||||
|
return this._charAtlas?.cacheCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
public setColors(colors: IColorSet): void {
|
public setColors(colors: IColorSet): void {
|
||||||
this._colors = colors;
|
this._colors = colors;
|
||||||
// Clear layers and force a full render
|
// Clear layers and force a full render
|
||||||
this._renderLayers.forEach(l => {
|
for (const l of this._renderLayers) {
|
||||||
l.setColors(this._terminal, this._colors);
|
l.setColors(this._terminal, this._colors);
|
||||||
l.reset(this._terminal);
|
l.reset(this._terminal);
|
||||||
});
|
}
|
||||||
|
|
||||||
this._rectangleRenderer.setColors();
|
this._rectangleRenderer.setColors();
|
||||||
this._glyphRenderer.setColors();
|
this._glyphRenderer.setColors();
|
||||||
@@ -131,10 +147,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
|
|
||||||
this._model.resize(this._terminal.cols, this._terminal.rows);
|
this._model.resize(this._terminal.cols, this._terminal.rows);
|
||||||
this._rectangleRenderer.onResize();
|
|
||||||
|
|
||||||
// Resize all render layers
|
// 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
|
// Resize the canvas
|
||||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||||
@@ -143,8 +160,15 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
||||||
|
|
||||||
// Resize the screen
|
// Resize the screen
|
||||||
this._core.screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||||
this._core.screenElement.style.height = `${this.dimensions.canvasHeight}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.setDimensions(this.dimensions);
|
||||||
this._glyphRenderer.onResize();
|
this._glyphRenderer.onResize();
|
||||||
|
|
||||||
@@ -159,30 +183,37 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public onBlur(): void {
|
public onBlur(): void {
|
||||||
this._renderLayers.forEach(l => l.onBlur(this._terminal));
|
for (const l of this._renderLayers) {
|
||||||
|
l.onBlur(this._terminal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFocus(): void {
|
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], end: [number, number], columnSelectMode: boolean): void {
|
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);
|
this._updateSelectionModel(start, end, columnSelectMode);
|
||||||
|
|
||||||
this._rectangleRenderer.updateSelection(this._model.selection, columnSelectMode);
|
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||||
this._glyphRenderer.updateSelection(this._model, columnSelectMode);
|
|
||||||
|
|
||||||
this._onRequestRefreshRows.fire({ start: 0, end: this._terminal.rows - 1 });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCursorMove(): void {
|
public onCursorMove(): void {
|
||||||
this._renderLayers.forEach(l => l.onCursorMove(this._terminal));
|
for (const l of this._renderLayers) {
|
||||||
|
l.onCursorMove(this._terminal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onOptionsChanged(): void {
|
public onOptionsChanged(): void {
|
||||||
this._renderLayers.forEach(l => l.onOptionsChanged(this._terminal));
|
for (const l of this._renderLayers) {
|
||||||
|
l.onOptionsChanged(this._terminal);
|
||||||
|
}
|
||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
this._refreshCharAtlas();
|
this._refreshCharAtlas();
|
||||||
}
|
}
|
||||||
@@ -208,8 +239,17 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this._glyphRenderer.setAtlas(this._charAtlas);
|
this._glyphRenderer.setAtlas(this._charAtlas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearCharAtlas(): void {
|
||||||
|
this._charAtlas?.clearTexture();
|
||||||
|
this._model.clear();
|
||||||
|
this._updateModel(0, this._terminal.rows - 1);
|
||||||
|
this._onRequestRedraw.fire({ start: 0, end: this._terminal.rows - 1 });
|
||||||
|
}
|
||||||
|
|
||||||
public clear(): void {
|
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 {
|
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||||
@@ -222,7 +262,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
public renderRows(start: number, end: number): void {
|
public renderRows(start: number, end: number): void {
|
||||||
if (!this._isAttached) {
|
if (!this._isAttached) {
|
||||||
if (document.body.contains(this._core.screenElement) && (<any>this._core)._charSizeService.width && (<any>this._core)._charSizeService.height) {
|
if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) {
|
||||||
this._updateDimensions();
|
this._updateDimensions();
|
||||||
this._refreshCharAtlas();
|
this._refreshCharAtlas();
|
||||||
this._isAttached = true;
|
this._isAttached = true;
|
||||||
@@ -232,11 +272,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update render layers
|
// 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
|
// Tell renderer the frame is beginning
|
||||||
if (this._glyphRenderer.beginFrame()) {
|
if (this._glyphRenderer.beginFrame()) {
|
||||||
this._model.clear();
|
this._model.clear();
|
||||||
|
this._updateSelectionModel(undefined, undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update model to reflect what's drawn
|
// Update model to reflect what's drawn
|
||||||
@@ -249,16 +292,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
private _updateModel(start: number, end: number): void {
|
private _updateModel(start: number, end: number): void {
|
||||||
const terminal = this._core;
|
const terminal = this._core;
|
||||||
|
let cell: ICellData = this._workCell;
|
||||||
|
|
||||||
for (let y = start; y <= end; y++) {
|
for (let y = start; y <= end; y++) {
|
||||||
const row = y + terminal.buffer.ydisp;
|
const row = y + terminal.buffer.ydisp;
|
||||||
const line = terminal.buffer.lines.get(row)!;
|
const line = terminal.buffer.lines.get(row)!;
|
||||||
this._model.lineLengths[y] = 0;
|
this._model.lineLengths[y] = 0;
|
||||||
|
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||||
for (let x = 0; x < terminal.cols; x++) {
|
for (let x = 0; x < terminal.cols; x++) {
|
||||||
line.loadCell(x, this._workCell);
|
line.loadCell(x, cell);
|
||||||
|
|
||||||
const chars = this._workCell.getChars();
|
// If true, indicates that the current character(s) to draw were joined.
|
||||||
let code = this._workCell.getCode();
|
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;
|
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||||
|
|
||||||
if (code !== NULL_CELL_CODE) {
|
if (code !== NULL_CELL_CODE) {
|
||||||
@@ -267,55 +335,78 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
// Nothing has changed, no updates needed
|
// Nothing has changed, no updates needed
|
||||||
if (this._model.cells[i] === code &&
|
if (this._model.cells[i] === code &&
|
||||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workCell.bg &&
|
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === cell.bg &&
|
||||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workCell.fg) {
|
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === cell.fg) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Flag combined chars with a bit mask so they're easily identifiable
|
// Flag combined chars with a bit mask so they're easily identifiable
|
||||||
if (chars.length > 1) {
|
if (chars.length > 1) {
|
||||||
code = code | COMBINED_CHAR_BIT_MASK;
|
code |= COMBINED_CHAR_BIT_MASK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache the results in the model
|
// Cache the results in the model
|
||||||
this._model.cells[i] = code;
|
this._model.cells[i] = code;
|
||||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workCell.bg;
|
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = cell.bg;
|
||||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workCell.fg;
|
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);
|
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], end: [number, number]): void {
|
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||||
const terminal = this._terminal;
|
const terminal = this._terminal;
|
||||||
|
|
||||||
// Selection does not exist
|
// Selection does not exist
|
||||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||||
this._model.clearSelection();
|
this._model.clearSelection();
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate from buffer position to viewport position
|
// Translate from buffer position to viewport position
|
||||||
const viewportStartRow = start[1] - terminal.buffer.viewportY;
|
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
||||||
const viewportEndRow = end[1] - terminal.buffer.viewportY;
|
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
||||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||||
|
|
||||||
// No need to draw the selection
|
// No need to draw the selection
|
||||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||||
this._model.clearSelection();
|
this._model.clearSelection();
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._model.selection.hasSelection = true;
|
this._model.selection.hasSelection = true;
|
||||||
|
this._model.selection.columnSelectMode = columnSelectMode;
|
||||||
this._model.selection.viewportStartRow = viewportStartRow;
|
this._model.selection.viewportStartRow = viewportStartRow;
|
||||||
this._model.selection.viewportEndRow = viewportEndRow;
|
this._model.selection.viewportEndRow = viewportEndRow;
|
||||||
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
||||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||||
this._model.selection.startCol = start[0];
|
this._model.selection.startCol = start[0];
|
||||||
this._model.selection.endCol = end[0];
|
this._model.selection.endCol = end[0];
|
||||||
|
|
||||||
|
this._rectangleRenderer.updateSelection(this._model.selection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -325,7 +416,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
// TODO: Acquire CharSizeService properly
|
// TODO: Acquire CharSizeService properly
|
||||||
|
|
||||||
// Perform a new measure if the CharMeasure dimensions are not yet available
|
// Perform a new measure if the CharMeasure dimensions are not yet available
|
||||||
if (!(<any>this._core)._charSizeService.width || !(<any>this._core)._charSizeService.height) {
|
if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,12 +427,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
|
|
||||||
// NOTE: ceil fixes sometime, floor does others :s
|
// NOTE: ceil fixes sometime, floor does others :s
|
||||||
|
|
||||||
this.dimensions.scaledCharWidth = Math.floor((<any>this._core)._charSizeService.width * this._devicePixelRatio);
|
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
|
||||||
|
|
||||||
// Calculate the scaled character height. Height is ceiled in case
|
// Calculate the scaled character height. Height is ceiled in case
|
||||||
// devicePixelRatio is a floating point number in order to ensure there is
|
// devicePixelRatio is a floating point number in order to ensure there is
|
||||||
// enough space to draw the character to the cell.
|
// enough space to draw the character to the cell.
|
||||||
this.dimensions.scaledCharHeight = Math.ceil((<any>this._core)._charSizeService.height * this._devicePixelRatio);
|
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
|
||||||
|
|
||||||
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
// Calculate the scaled cell height, if lineHeight is not 1 then the value
|
||||||
// will be floored because since lineHeight can never be lower then 1, there
|
// will be floored because since lineHeight can never be lower then 1, there
|
||||||
@@ -390,3 +481,49 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
|||||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
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()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export function generateConfig(scaledCharWidth: number, scaledCharHeight: number
|
|||||||
background: colors.background,
|
background: colors.background,
|
||||||
cursor: NULL_COLOR,
|
cursor: NULL_COLOR,
|
||||||
cursorAccent: NULL_COLOR,
|
cursorAccent: NULL_COLOR,
|
||||||
selection: NULL_COLOR,
|
selectionTransparent: NULL_COLOR,
|
||||||
selectionOpaque: NULL_COLOR,
|
selectionOpaque: NULL_COLOR,
|
||||||
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
// For the static char atlas, we only use the first 16 colors, but we need all 256 for the
|
||||||
// dynamic character atlas.
|
// dynamic character atlas.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { throwIfFalsy } from '../WebglUtils';
|
|||||||
import { IColor } from 'browser/Types';
|
import { IColor } from 'browser/Types';
|
||||||
import { IDisposable } from 'xterm';
|
import { IDisposable } from 'xterm';
|
||||||
import { AttributeData } from 'common/buffer/AttributeData';
|
import { AttributeData } from 'common/buffer/AttributeData';
|
||||||
import { toCss, ensureContrastRatioRgba } from 'browser/Color';
|
import { channels, rgba } from 'browser/Color';
|
||||||
|
|
||||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
// 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.
|
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||||
@@ -80,15 +80,12 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
// 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
|
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||||
// set.
|
// 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 = document.createElement('canvas');
|
||||||
this._tmpCanvas.width = this._config.scaledCharWidth * 2 + TMP_CANVAS_GLYPH_PADDING * 2;
|
this._tmpCanvas.width = this._config.scaledCharWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||||
this._tmpCanvas.height = this._config.scaledCharHeight + 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._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||||
|
|
||||||
// This is useful for debugging
|
|
||||||
document.body.appendChild(this.cacheCanvas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
@@ -104,7 +101,7 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _doWarmUp(): void {
|
private _doWarmUp(): void {
|
||||||
// Pre-fill with ASCII 33-126
|
// Pre-fill with ASCII 33-126
|
||||||
for (let i = 33; i < 126; i++) {
|
for (let i = 33; i < 126; i++) {
|
||||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
|
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
|
||||||
@@ -118,17 +115,26 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
|
|
||||||
public beginFrame(): boolean {
|
public beginFrame(): boolean {
|
||||||
if (this._currentRowY > TEXTURE_CAPACITY) {
|
if (this._currentRowY > TEXTURE_CAPACITY) {
|
||||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
this.clearTexture();
|
||||||
this._cacheMap = {};
|
this.warmUp();
|
||||||
this._currentRowHeight = 0;
|
|
||||||
this._currentRowX = 0;
|
|
||||||
this._currentRowY = 0;
|
|
||||||
this._doWarmUp();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public clearTexture(): void {
|
||||||
|
if (this._currentRowX === 0 && this._currentRowY === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||||
|
this._cacheMap = {};
|
||||||
|
this._cacheMapCombined = {};
|
||||||
|
this._currentRowHeight = 0;
|
||||||
|
this._currentRowX = 0;
|
||||||
|
this._currentRowY = 0;
|
||||||
|
this._didWarmUp = false;
|
||||||
|
}
|
||||||
|
|
||||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
|
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number): IRasterizedGlyph {
|
||||||
let rasterizedGlyphSet = this._cacheMapCombined[chars];
|
let rasterizedGlyphSet = this._cacheMapCombined[chars];
|
||||||
if (!rasterizedGlyphSet) {
|
if (!rasterizedGlyphSet) {
|
||||||
@@ -224,7 +230,7 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
return this._getColorFromAnsiIndex(fgColor).css;
|
return this._getColorFromAnsiIndex(fgColor).css;
|
||||||
case Attributes.CM_RGB:
|
case Attributes.CM_RGB:
|
||||||
const arr = AttributeData.toColorRGB(fgColor);
|
const arr = AttributeData.toColorRGB(fgColor);
|
||||||
return toCss(arr[0], arr[1], arr[2]);
|
return channels.toCss(arr[0], arr[1], arr[2]);
|
||||||
case Attributes.CM_DEFAULT:
|
case Attributes.CM_DEFAULT:
|
||||||
default:
|
default:
|
||||||
if (inverse) {
|
if (inverse) {
|
||||||
@@ -287,14 +293,14 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
|
|
||||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
||||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
|
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
|
||||||
const result = ensureContrastRatioRgba(bgRgba, fgRgba, this._config.minimumContrastRatio);
|
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
this._config.colors.contrastCache.setCss(bg, fg, null);
|
this._config.colors.contrastCache.setCss(bg, fg, null);
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const css = toCss(
|
const css = channels.toCss(
|
||||||
(result >> 24) & 0xFF,
|
(result >> 24) & 0xFF,
|
||||||
(result >> 16) & 0xFF,
|
(result >> 16) & 0xFF,
|
||||||
(result >> 8) & 0xFF
|
(result >> 8) & 0xFF
|
||||||
@@ -311,11 +317,23 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
|
|
||||||
this.hasCanvasChanged = true;
|
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._tmpCtx.save();
|
||||||
|
|
||||||
this._workAttributeData.fg = fg;
|
this._workAttributeData.fg = fg;
|
||||||
this._workAttributeData.bg = bg;
|
this._workAttributeData.bg = bg;
|
||||||
|
|
||||||
|
const invisible = !!this._workAttributeData.isInvisible();
|
||||||
|
if (invisible) {
|
||||||
|
return NULL_RASTERIZED_GLYPH;
|
||||||
|
}
|
||||||
|
|
||||||
const bold = !!this._workAttributeData.isBold();
|
const bold = !!this._workAttributeData.isBold();
|
||||||
const inverse = !!this._workAttributeData.isInverse();
|
const inverse = !!this._workAttributeData.isInverse();
|
||||||
const dim = !!this._workAttributeData.isDim();
|
const dim = !!this._workAttributeData.isDim();
|
||||||
@@ -347,7 +365,7 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
const fontStyle = italic ? 'italic' : '';
|
const fontStyle = italic ? 'italic' : '';
|
||||||
this._tmpCtx.font =
|
this._tmpCtx.font =
|
||||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||||
this._tmpCtx.textBaseline = 'top';
|
this._tmpCtx.textBaseline = 'ideographic';
|
||||||
|
|
||||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold);
|
||||||
|
|
||||||
@@ -356,8 +374,22 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
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 the character
|
// Draw the character
|
||||||
this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING);
|
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||||
this._tmpCtx.restore();
|
this._tmpCtx.restore();
|
||||||
|
|
||||||
// clear the background from the character to avoid issues with drawing over the previous
|
// clear the background from the character to avoid issues with drawing over the previous
|
||||||
@@ -380,7 +412,7 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
return NULL_RASTERIZED_GLYPH;
|
return NULL_RASTERIZED_GLYPH;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox);
|
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, isPowerlineGlyph);
|
||||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||||
|
|
||||||
// Check if there is enough room in the current row and go to next if needed
|
// Check if there is enough room in the current row and go to next if needed
|
||||||
@@ -413,11 +445,13 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
* @param imageData The image data to read.
|
* @param imageData The image data to read.
|
||||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
* @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): IRasterizedGlyph {
|
||||||
boundingBox.top = 0;
|
boundingBox.top = 0;
|
||||||
|
const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height;
|
||||||
|
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||||
let found = false;
|
let found = false;
|
||||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||||
if (imageData.data[alphaOffset] !== 0) {
|
if (imageData.data[alphaOffset] !== 0) {
|
||||||
boundingBox.top = y;
|
boundingBox.top = y;
|
||||||
@@ -431,8 +465,8 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
}
|
}
|
||||||
boundingBox.left = 0;
|
boundingBox.left = 0;
|
||||||
found = false;
|
found = false;
|
||||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||||
if (imageData.data[alphaOffset] !== 0) {
|
if (imageData.data[alphaOffset] !== 0) {
|
||||||
boundingBox.left = x;
|
boundingBox.left = x;
|
||||||
@@ -444,10 +478,10 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boundingBox.right = this._tmpCanvas.width;
|
boundingBox.right = width;
|
||||||
found = false;
|
found = false;
|
||||||
for (let x = this._tmpCanvas.width - 1; x >= 0; x--) {
|
for (let x = width - 1; x >= 0; x--) {
|
||||||
for (let y = 0; y < this._tmpCanvas.height; y++) {
|
for (let y = 0; y < height; y++) {
|
||||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||||
if (imageData.data[alphaOffset] !== 0) {
|
if (imageData.data[alphaOffset] !== 0) {
|
||||||
boundingBox.right = x;
|
boundingBox.right = x;
|
||||||
@@ -459,10 +493,10 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
boundingBox.bottom = this._tmpCanvas.height;
|
boundingBox.bottom = height;
|
||||||
found = false;
|
found = false;
|
||||||
for (let y = this._tmpCanvas.height - 1; y >= 0; y--) {
|
for (let y = height - 1; y >= 0; y--) {
|
||||||
for (let x = 0; x < this._tmpCanvas.width; x++) {
|
for (let x = 0; x < width; x++) {
|
||||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||||
if (imageData.data[alphaOffset] !== 0) {
|
if (imageData.data[alphaOffset] !== 0) {
|
||||||
boundingBox.bottom = y;
|
boundingBox.bottom = y;
|
||||||
@@ -486,8 +520,8 @@ export class WebglCharAtlas implements IDisposable {
|
|||||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||||
},
|
},
|
||||||
offset: {
|
offset: {
|
||||||
x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING,
|
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING),
|
||||||
y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING
|
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _initCanvas(): void {
|
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
|
// Draw the background if this is an opaque layer
|
||||||
if (!this._alpha) {
|
if (!this._alpha) {
|
||||||
this._clearAll();
|
this._clearAll();
|
||||||
@@ -58,7 +58,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
public onFocus(terminal: Terminal): void {}
|
public onFocus(terminal: Terminal): void {}
|
||||||
public onCursorMove(terminal: Terminal): void {}
|
public onCursorMove(terminal: Terminal): void {}
|
||||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||||
public onSelectionChanged(terminal: Terminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||||
|
|
||||||
public setColors(terminal: Terminal, colorSet: IColorSet): void {
|
public setColors(terminal: Terminal, colorSet: IColorSet): void {
|
||||||
this._refreshCharAtlas(terminal, colorSet);
|
this._refreshCharAtlas(terminal, colorSet);
|
||||||
@@ -74,7 +74,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
const oldCanvas = this._canvas;
|
const oldCanvas = this._canvas;
|
||||||
this._alpha = alpha;
|
this._alpha = alpha;
|
||||||
// Cloning preserves properties
|
// Cloning preserves properties
|
||||||
this._canvas = <HTMLCanvasElement>this._canvas.cloneNode();
|
this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
|
||||||
this._initCanvas();
|
this._initCanvas();
|
||||||
this._container.replaceChild(this._canvas, oldCanvas);
|
this._container.replaceChild(this._canvas, oldCanvas);
|
||||||
|
|
||||||
@@ -127,10 +127,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this._scaledCellWidth,
|
x * this._scaledCellWidth,
|
||||||
y * this._scaledCellHeight,
|
y * this._scaledCellHeight,
|
||||||
width * this._scaledCellWidth,
|
width * this._scaledCellWidth,
|
||||||
height * this._scaledCellHeight);
|
height * this._scaledCellHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -141,10 +141,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this._scaledCellWidth,
|
x * this._scaledCellWidth,
|
||||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||||
width * this._scaledCellWidth,
|
width * this._scaledCellWidth,
|
||||||
window.devicePixelRatio);
|
window.devicePixelRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -153,12 +153,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
* @param x The column to fill.
|
* @param x The column to fill.
|
||||||
* @param y The row to fill.
|
* @param y The row to fill.
|
||||||
*/
|
*/
|
||||||
protected _fillLeftLineAtCell(x: number, y: number): void {
|
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this._scaledCellWidth,
|
x * this._scaledCellWidth,
|
||||||
y * this._scaledCellHeight,
|
y * this._scaledCellHeight,
|
||||||
window.devicePixelRatio,
|
window.devicePixelRatio * width,
|
||||||
this._scaledCellHeight);
|
this._scaledCellHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,10 +170,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||||
this._ctx.lineWidth = window.devicePixelRatio;
|
this._ctx.lineWidth = window.devicePixelRatio;
|
||||||
this._ctx.strokeRect(
|
this._ctx.strokeRect(
|
||||||
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
||||||
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
||||||
width * this._scaledCellWidth - window.devicePixelRatio,
|
width * this._scaledCellWidth - window.devicePixelRatio,
|
||||||
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -198,17 +198,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||||
if (this._alpha) {
|
if (this._alpha) {
|
||||||
this._ctx.clearRect(
|
this._ctx.clearRect(
|
||||||
x * this._scaledCellWidth,
|
x * this._scaledCellWidth,
|
||||||
y * this._scaledCellHeight,
|
y * this._scaledCellHeight,
|
||||||
width * this._scaledCellWidth,
|
width * this._scaledCellWidth,
|
||||||
height * this._scaledCellHeight);
|
height * this._scaledCellHeight);
|
||||||
} else {
|
} else {
|
||||||
this._ctx.fillStyle = this._colors.background.css;
|
this._ctx.fillStyle = this._colors.background.css;
|
||||||
this._ctx.fillRect(
|
this._ctx.fillRect(
|
||||||
x * this._scaledCellWidth,
|
x * this._scaledCellWidth,
|
||||||
y * this._scaledCellHeight,
|
y * this._scaledCellHeight,
|
||||||
width * this._scaledCellWidth,
|
width * this._scaledCellWidth,
|
||||||
height * this._scaledCellHeight);
|
height * this._scaledCellHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
*/
|
*/
|
||||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
||||||
this._ctx.font = this._getFont(terminal, false, false);
|
this._ctx.font = this._getFont(terminal, false, false);
|
||||||
this._ctx.textBaseline = 'middle';
|
this._ctx.textBaseline = 'ideographic';
|
||||||
this._clipRow(terminal, y);
|
this._clipRow(terminal, y);
|
||||||
this._ctx.fillText(
|
this._ctx.fillText(
|
||||||
cell.getChars(),
|
cell.getChars(),
|
||||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2);
|
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,10 +240,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
|||||||
private _clipRow(terminal: Terminal, y: number): void {
|
private _clipRow(terminal: Terminal, y: number): void {
|
||||||
this._ctx.beginPath();
|
this._ctx.beginPath();
|
||||||
this._ctx.rect(
|
this._ctx.rect(
|
||||||
0,
|
0,
|
||||||
y * this._scaledCellHeight,
|
y * this._scaledCellHeight,
|
||||||
terminal.cols * this._scaledCellWidth,
|
terminal.cols * this._scaledCellWidth,
|
||||||
this._scaledCellHeight);
|
this._scaledCellHeight);
|
||||||
this._ctx.clip();
|
this._ctx.clip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { BaseRenderLayer } from './BaseRenderLayer';
|
|||||||
import { ICellData } from 'common/Types';
|
import { ICellData } from 'common/Types';
|
||||||
import { CellData } from 'common/buffer/CellData';
|
import { CellData } from 'common/buffer/CellData';
|
||||||
import { IColorSet } from 'browser/Types';
|
import { IColorSet } from 'browser/Types';
|
||||||
import { IRenderDimensions, IRequestRefreshRowsEvent } from 'browser/renderer/Types';
|
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||||
import { IEventEmitter } from 'common/EventEmitter';
|
import { IEventEmitter } from 'common/EventEmitter';
|
||||||
|
|
||||||
interface ICursorState {
|
interface ICursorState {
|
||||||
@@ -34,7 +34,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
zIndex: number,
|
zIndex: number,
|
||||||
colors: IColorSet,
|
colors: IColorSet,
|
||||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRefreshRowsEvent>
|
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>
|
||||||
) {
|
) {
|
||||||
super(container, 'cursor', zIndex, true, colors);
|
super(container, 'cursor', zIndex, true, colors);
|
||||||
this._state = {
|
this._state = {
|
||||||
@@ -76,14 +76,14 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
if (this._cursorBlinkStateManager) {
|
if (this._cursorBlinkStateManager) {
|
||||||
this._cursorBlinkStateManager.pause();
|
this._cursorBlinkStateManager.pause();
|
||||||
}
|
}
|
||||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
|
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFocus(terminal: Terminal): void {
|
public onFocus(terminal: Terminal): void {
|
||||||
if (this._cursorBlinkStateManager) {
|
if (this._cursorBlinkStateManager) {
|
||||||
this._cursorBlinkStateManager.resume(terminal);
|
this._cursorBlinkStateManager.resume(terminal);
|
||||||
} else {
|
} else {
|
||||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
|
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
// Request a refresh from the terminal as management of rendering is being
|
// Request a refresh from the terminal as management of rendering is being
|
||||||
// moved back to the terminal
|
// moved back to the terminal
|
||||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.cursorY, end: terminal.buffer.cursorY });
|
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onCursorMove(terminal: Terminal): void {
|
public onCursorMove(terminal: Terminal): void {
|
||||||
@@ -125,8 +125,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cursorY = terminal.buffer.baseY + terminal.buffer.cursorY;
|
const cursorY = terminal.buffer.active.baseY + terminal.buffer.active.cursorY;
|
||||||
const viewportRelativeCursorY = cursorY - terminal.buffer.viewportY;
|
const viewportRelativeCursorY = cursorY - terminal.buffer.active.viewportY;
|
||||||
|
|
||||||
|
// in case cursor.x == cols adjust visual cursor to cols - 1
|
||||||
|
const cursorX = Math.min(terminal.buffer.active.cursorX, terminal.cols - 1);
|
||||||
|
|
||||||
// Don't draw the cursor if it's off-screen
|
// Don't draw the cursor if it's off-screen
|
||||||
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) {
|
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= terminal.rows) {
|
||||||
@@ -135,7 +138,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Need fast buffer API for loading cell
|
// TODO: Need fast buffer API for loading cell
|
||||||
(terminal as any)._core.buffer.lines.get(cursorY).loadCell(terminal.buffer.cursorX, this._cell);
|
(terminal as any)._core.buffer.lines.get(cursorY).loadCell(cursorX, this._cell);
|
||||||
if (this._cell.content === undefined) {
|
if (this._cell.content === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -146,12 +149,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
this._ctx.fillStyle = this._colors.cursor.css;
|
this._ctx.fillStyle = this._colors.cursor.css;
|
||||||
const cursorStyle = terminal.getOption('cursorStyle');
|
const cursorStyle = terminal.getOption('cursorStyle');
|
||||||
if (cursorStyle && cursorStyle !== 'block') {
|
if (cursorStyle && cursorStyle !== 'block') {
|
||||||
this._cursorRenderers[cursorStyle](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||||
} else {
|
} else {
|
||||||
this._renderBlurCursor(terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||||
}
|
}
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
this._state.x = terminal.buffer.cursorX;
|
this._state.x = cursorX;
|
||||||
this._state.y = viewportRelativeCursorY;
|
this._state.y = viewportRelativeCursorY;
|
||||||
this._state.isFocused = false;
|
this._state.isFocused = false;
|
||||||
this._state.style = cursorStyle;
|
this._state.style = cursorStyle;
|
||||||
@@ -167,7 +170,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
if (this._state) {
|
if (this._state) {
|
||||||
// The cursor is already in the correct spot, don't redraw
|
// The cursor is already in the correct spot, don't redraw
|
||||||
if (this._state.x === terminal.buffer.cursorX &&
|
if (this._state.x === cursorX &&
|
||||||
this._state.y === viewportRelativeCursorY &&
|
this._state.y === viewportRelativeCursorY &&
|
||||||
this._state.isFocused === isTerminalFocused(terminal) &&
|
this._state.isFocused === isTerminalFocused(terminal) &&
|
||||||
this._state.style === terminal.getOption('cursorStyle') &&
|
this._state.style === terminal.getOption('cursorStyle') &&
|
||||||
@@ -178,10 +181,10 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, terminal.buffer.cursorX, viewportRelativeCursorY, this._cell);
|
this._cursorRenderers[terminal.getOption('cursorStyle') || 'block'](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
|
|
||||||
this._state.x = terminal.buffer.cursorX;
|
this._state.x = cursorX;
|
||||||
this._state.y = viewportRelativeCursorY;
|
this._state.y = viewportRelativeCursorY;
|
||||||
this._state.isFocused = false;
|
this._state.isFocused = false;
|
||||||
this._state.style = terminal.getOption('cursorStyle');
|
this._state.style = terminal.getOption('cursorStyle');
|
||||||
@@ -204,7 +207,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
|||||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||||
this._ctx.save();
|
this._ctx.save();
|
||||||
this._ctx.fillStyle = this._colors.cursor.css;
|
this._ctx.fillStyle = this._colors.cursor.css;
|
||||||
this._fillLeftLineAtCell(x, y);
|
this._fillLeftLineAtCell(x, y, terminal.getOption('cursorWidth'));
|
||||||
this._ctx.restore();
|
this._ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +302,7 @@ class CursorBlinkStateManager {
|
|||||||
// the regular interval is setup in order to support restarting the blink
|
// the regular interval is setup in order to support restarting the blink
|
||||||
// animation in a lightweight way (without thrashing clearInterval and
|
// animation in a lightweight way (without thrashing clearInterval and
|
||||||
// setInterval).
|
// setInterval).
|
||||||
this._blinkStartTimeout = <number><any>setTimeout(() => {
|
this._blinkStartTimeout = window.setTimeout(() => {
|
||||||
// Check if another animation restart was requested while this was being
|
// Check if another animation restart was requested while this was being
|
||||||
// started
|
// started
|
||||||
if (this._animationTimeRestarted) {
|
if (this._animationTimeRestarted) {
|
||||||
@@ -319,7 +322,7 @@ class CursorBlinkStateManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Setup the blink interval
|
// Setup the blink interval
|
||||||
this._blinkInterval = <number><any>setInterval(() => {
|
this._blinkInterval = window.setInterval(() => {
|
||||||
// Adjust the animation time if it was restarted
|
// Adjust the animation time if it was restarted
|
||||||
if (this._animationTimeRestarted) {
|
if (this._animationTimeRestarted) {
|
||||||
// calc time diff
|
// calc time diff
|
||||||
@@ -357,6 +360,9 @@ class CursorBlinkStateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public resume(terminal: Terminal): void {
|
public resume(terminal: Terminal): void {
|
||||||
|
// Clear out any existing timers just in case
|
||||||
|
this.pause();
|
||||||
|
|
||||||
this._animationTimeRestarted = undefined;
|
this._animationTimeRestarted = undefined;
|
||||||
this._restartInterval();
|
this._restartInterval();
|
||||||
this.restartBlinkAnimation(terminal);
|
this.restartBlinkAnimation(terminal);
|
||||||
|
|||||||
@@ -3,21 +3,23 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ILinkifierAccessor } from '../../../../src/Types';
|
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from 'xterm';
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||||
import { is256Color } from '../atlas/CharAtlasUtils';
|
import { is256Color } from '../atlas/CharAtlasUtils';
|
||||||
import { IColorSet, ILinkifierEvent } from 'browser/Types';
|
import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||||
|
|
||||||
export class LinkRenderLayer extends BaseRenderLayer {
|
export class LinkRenderLayer extends BaseRenderLayer {
|
||||||
private _state: ILinkifierEvent | undefined;
|
private _state: ILinkifierEvent | undefined;
|
||||||
|
|
||||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ILinkifierAccessor) {
|
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) {
|
||||||
super(container, 'link', zIndex, true, colors);
|
super(container, 'link', zIndex, true, colors);
|
||||||
terminal.linkifier.onLinkHover(e => this._onLinkHover(e));
|
terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||||
terminal.linkifier.onLinkLeave(e => this._onLinkLeave(e));
|
terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||||
|
|
||||||
|
terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||||
|
terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||||
@@ -42,7 +44,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onLinkHover(e: ILinkifierEvent): void {
|
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||||
this._ctx.fillStyle = this._colors.background.css;
|
this._ctx.fillStyle = this._colors.background.css;
|
||||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||||
@@ -66,7 +68,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
this._state = e;
|
this._state = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||||
this._clearCurrentLink();
|
this._clearCurrentLink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export interface IRenderLayer extends IDisposable {
|
|||||||
/**
|
/**
|
||||||
* Calls when the selection changes.
|
* Calls when the selection changes.
|
||||||
*/
|
*/
|
||||||
onSelectionChanged(terminal: Terminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers a handler to join characters to render as a group
|
* Registers a handler to join characters to render as a group
|
||||||
|
|||||||
@@ -12,17 +12,28 @@
|
|||||||
"removeComments": true,
|
"removeComments": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"common/*": [ "../../../src/common/*" ],
|
"common/*": [
|
||||||
"browser/*": [ "../../../src/browser/*" ]
|
"../../../src/common/*"
|
||||||
|
],
|
||||||
|
"browser/*": [
|
||||||
|
"../../../src/browser/*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
"../../../typings/xterm.d.ts"
|
"../../../typings/xterm.d.ts"
|
||||||
],
|
],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "../../../src/common" },
|
{
|
||||||
{ "path": "../../../src/browser" }
|
"path": "../../../src/common"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "../../../src/browser"
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+239
-109
@@ -3,21 +3,27 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as puppeteer from 'puppeteer';
|
import { ITerminalOptions } from '../../../src/common/Types';
|
||||||
import { ITerminalOptions } from '../../../src/Types';
|
|
||||||
import { ITheme } from 'xterm';
|
import { ITheme } from 'xterm';
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import deepEqual = require('deep-equal');
|
import { openTerminal, pollFor, writeSync, getBrowserType } 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: puppeteer.Browser;
|
let browser: Browser;
|
||||||
let page: puppeteer.Page;
|
let page: Page;
|
||||||
const width = 800;
|
const width = 800;
|
||||||
const height = 600;
|
const height = 600;
|
||||||
|
|
||||||
describe('WebGL Renderer Integration Tests', function(): void {
|
describe('WebGL Renderer Integration Tests', async () => {
|
||||||
it('dispose removes renderer canvases', async () => {
|
const browserType = getBrowserType();
|
||||||
|
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;
|
||||||
|
|
||||||
|
itWebgl('dispose removes renderer canvases', async function(): Promise<void> {
|
||||||
await setupBrowser();
|
await setupBrowser();
|
||||||
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
|
assert.equal(await page.evaluate(`document.querySelectorAll('.xterm canvas').length`), 3);
|
||||||
await page.evaluate(`addon.dispose()`);
|
await page.evaluate(`addon.dispose()`);
|
||||||
@@ -26,11 +32,13 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('colors', () => {
|
describe('colors', () => {
|
||||||
before(async () => setupBrowser());
|
if (areTestsEnabled) {
|
||||||
after(async () => browser.close());
|
before(async () => setupBrowser());
|
||||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
after(async () => browser.close());
|
||||||
|
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||||
|
}
|
||||||
|
|
||||||
it('foreground 0-15', async () => {
|
itWebgl('foreground 0-15', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
black: '#010203',
|
black: '#010203',
|
||||||
red: '#040506',
|
red: '#040506',
|
||||||
@@ -42,7 +50,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
white: '#161718'
|
white: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
await writeSync(page, `\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█`);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -53,7 +61,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground 0-7 drawBoldTextInBrightColors', async () => {
|
itWebgl('foreground 0-7 drawBoldTextInBrightColors', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
brightBlack: '#010203',
|
brightBlack: '#010203',
|
||||||
brightRed: '#040506',
|
brightRed: '#040506',
|
||||||
@@ -68,7 +76,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||||
window.term.setOption('drawBoldTextInBrightColors', true);
|
window.term.setOption('drawBoldTextInBrightColors', true);
|
||||||
`);
|
`);
|
||||||
await writeSync(`\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
|
await writeSync(page, `\\x1b[1;30m█\\x1b[1;31m█\\x1b[1;32m█\\x1b[1;33m█\\x1b[1;34m█\\x1b[1;35m█\\x1b[1;36m█\\x1b[1;37m█`);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -79,7 +87,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background 0-15', async () => {
|
itWebgl('background 0-15', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
black: '#010203',
|
black: '#010203',
|
||||||
red: '#040506',
|
red: '#040506',
|
||||||
@@ -91,7 +99,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
white: '#161718'
|
white: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
await writeSync(page, `\\x1b[40m \\x1b[41m \\x1b[42m \\x1b[43m \\x1b[44m \\x1b[45m \\x1b[46m \\x1b[47m `);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -102,7 +110,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground 0-15 inverse', async () => {
|
itWebgl('foreground 0-15 inverse', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
black: '#010203',
|
black: '#010203',
|
||||||
red: '#040506',
|
red: '#040506',
|
||||||
@@ -114,7 +122,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
white: '#161718'
|
white: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
|
await writeSync(page, `\\x1b[7;30m \\x1b[7;31m \\x1b[7;32m \\x1b[7;33m \\x1b[7;34m \\x1b[7;35m \\x1b[7;36m \\x1b[7;37m `);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -125,7 +133,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background 0-15 inverse', async () => {
|
itWebgl('background 0-15 inverse', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
black: '#010203',
|
black: '#010203',
|
||||||
red: '#040506',
|
red: '#040506',
|
||||||
@@ -137,7 +145,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
white: '#161718'
|
white: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
|
await writeSync(page, `\\x1b[7;40m█\\x1b[7;41m█\\x1b[7;42m█\\x1b[7;43m█\\x1b[7;44m█\\x1b[7;45m█\\x1b[7;46m█\\x1b[7;47m█`);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -148,7 +156,53 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground 0-15 bright', async () => {
|
itWebgl('foreground 0-15 inivisible', async () => {
|
||||||
|
const theme: ITheme = {
|
||||||
|
black: '#010203',
|
||||||
|
red: '#040506',
|
||||||
|
green: '#070809',
|
||||||
|
yellow: '#0a0b0c',
|
||||||
|
blue: '#0d0e0f',
|
||||||
|
magenta: '#101112',
|
||||||
|
cyan: '#131415',
|
||||||
|
white: '#161718'
|
||||||
|
};
|
||||||
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
|
await writeSync(page, `\\x1b[8;30m \\x1b[8;31m \\x1b[8;32m \\x1b[8;33m \\x1b[8;34m \\x1b[8;35m \\x1b[8;36m \\x1b[8;37m `);
|
||||||
|
await pollFor(page, () => getCellColor(1, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(2, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(3, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(4, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(5, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(6, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(7, 1), [0, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(8, 1), [0, 0, 0, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('background 0-15 inivisible', async () => {
|
||||||
|
const theme: ITheme = {
|
||||||
|
black: '#010203',
|
||||||
|
red: '#040506',
|
||||||
|
green: '#070809',
|
||||||
|
yellow: '#0a0b0c',
|
||||||
|
blue: '#0d0e0f',
|
||||||
|
magenta: '#101112',
|
||||||
|
cyan: '#131415',
|
||||||
|
white: '#161718'
|
||||||
|
};
|
||||||
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
|
await writeSync(page, `\\x1b[8;40m█\\x1b[8;41m█\\x1b[8;42m█\\x1b[8;43m█\\x1b[8;44m█\\x1b[8;45m█\\x1b[8;46m█\\x1b[8;47m█`);
|
||||||
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(4, 1), [10, 11, 12, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(5, 1), [13, 14, 15, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(6, 1), [16, 17, 18, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(7, 1), [19, 20, 21, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('foreground 0-15 bright', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
brightBlack: '#010203',
|
brightBlack: '#010203',
|
||||||
brightRed: '#040506',
|
brightRed: '#040506',
|
||||||
@@ -160,7 +214,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
brightWhite: '#161718'
|
brightWhite: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
await writeSync(page, `\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -171,7 +225,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background 0-15 bright', async () => {
|
itWebgl('background 0-15 bright', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
brightBlack: '#010203',
|
brightBlack: '#010203',
|
||||||
brightRed: '#040506',
|
brightRed: '#040506',
|
||||||
@@ -183,7 +237,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
brightWhite: '#161718'
|
brightWhite: '#161718'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
await writeSync(`\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
await writeSync(page, `\\x1b[100m \\x1b[101m \\x1b[102m \\x1b[103m \\x1b[104m \\x1b[105m \\x1b[106m \\x1b[107m `);
|
||||||
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [1, 2, 3, 255]);
|
||||||
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
await pollFor(page, () => getCellColor(2, 1), [4, 5, 6, 255]);
|
||||||
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
await pollFor(page, () => getCellColor(3, 1), [7, 8, 9, 255]);
|
||||||
@@ -194,7 +248,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
await pollFor(page, () => getCellColor(8, 1), [22, 23, 24, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground 16-255', async () => {
|
itWebgl('foreground 16-255', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -202,7 +256,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||||
@@ -214,7 +268,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background 16-255', async () => {
|
itWebgl('background 16-255', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -222,7 +276,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||||
@@ -234,7 +288,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground 16-255 inverse', async () => {
|
itWebgl('foreground 16-255 inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -242,7 +296,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||||
@@ -254,7 +308,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background 16-255 inverse', async () => {
|
itWebgl('background 16-255 inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -262,7 +316,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 240 / 16; y++) {
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||||
@@ -274,7 +328,43 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color red', async () => {
|
itWebgl('foreground 16-255 invisible', async () => {
|
||||||
|
let data = '';
|
||||||
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
data += `\\x1b[8;38;5;${16 + y * 16 + x}m \x1b[0m`;
|
||||||
|
}
|
||||||
|
data += '\\r\\n';
|
||||||
|
}
|
||||||
|
await writeSync(page, data);
|
||||||
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('background 16-255 invisible', async () => {
|
||||||
|
let data = '';
|
||||||
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
data += `\\x1b[8;48;5;${16 + y * 16 + x}m█\x1b[0m`;
|
||||||
|
}
|
||||||
|
data += '\\r\\n';
|
||||||
|
}
|
||||||
|
await writeSync(page, data);
|
||||||
|
for (let y = 0; y < 240 / 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||||
|
const r = parseInt(cssColor.substr(1, 2), 16);
|
||||||
|
const g = parseInt(cssColor.substr(3, 2), 16);
|
||||||
|
const b = parseInt(cssColor.substr(5, 2), 16);
|
||||||
|
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('foreground true color red', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -283,7 +373,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -292,7 +382,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color red', async () => {
|
itWebgl('background true color red', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -301,7 +391,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -310,7 +400,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color green', async () => {
|
itWebgl('foreground true color green', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -319,7 +409,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -328,7 +418,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color green', async () => {
|
itWebgl('background true color green', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -337,7 +427,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -346,7 +436,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color blue', async () => {
|
itWebgl('foreground true color blue', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -355,7 +445,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -364,7 +454,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color blue', async () => {
|
itWebgl('background true color blue', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -373,7 +463,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -382,7 +472,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color grey', async () => {
|
itWebgl('foreground true color grey', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -391,7 +481,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -400,7 +490,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color grey', async () => {
|
itWebgl('background true color grey', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -409,7 +499,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -418,7 +508,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color red inverse', async function(): Promise<void> {
|
itWebgl('foreground true color red inverse', async function(): Promise<void> {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -427,7 +517,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -436,7 +526,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color red inverse', async function(): Promise<void> {
|
itWebgl('background true color red inverse', async function(): Promise<void> {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -445,7 +535,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -454,7 +544,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color green inverse', async () => {
|
itWebgl('foreground true color green inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -463,7 +553,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -472,7 +562,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color green inverse', async () => {
|
itWebgl('background true color green inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -481,7 +571,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -490,7 +580,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color blue inverse', async () => {
|
itWebgl('foreground true color blue inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -499,7 +589,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -508,7 +598,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color blue inverse', async () => {
|
itWebgl('background true color blue inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -517,7 +607,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -526,7 +616,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('foreground true color grey inverse', async () => {
|
itWebgl('foreground true color grey inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -535,7 +625,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -544,7 +634,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('background true color grey inverse', async () => {
|
itWebgl('background true color grey inverse', async () => {
|
||||||
let data = '';
|
let data = '';
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
@@ -553,7 +643,42 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
}
|
}
|
||||||
data += '\\r\\n';
|
data += '\\r\\n';
|
||||||
}
|
}
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
|
for (let y = 0; y < 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
const i = y * 16 + x;
|
||||||
|
await pollFor(page, () => getCellColor(x + 1, y + 1), [i, i, i, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('foreground true color grey invisible', async () => {
|
||||||
|
let data = '';
|
||||||
|
for (let y = 0; y < 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
const i = y * 16 + x;
|
||||||
|
data += `\\x1b[8;38;2;${i};${i};${i}m \x1b[0m`;
|
||||||
|
}
|
||||||
|
data += '\\r\\n';
|
||||||
|
}
|
||||||
|
await writeSync(page, data);
|
||||||
|
for (let y = 0; y < 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
await pollFor(page, () => getCellColor(x + 1, y + 1), [0, 0, 0, 255]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
itWebgl('background true color grey invisible', async () => {
|
||||||
|
let data = '';
|
||||||
|
for (let y = 0; y < 16; y++) {
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
const i = y * 16 + x;
|
||||||
|
data += `\\x1b[8;48;2;${i};${i};${i}m█\x1b[0m`;
|
||||||
|
}
|
||||||
|
data += '\\r\\n';
|
||||||
|
}
|
||||||
|
await writeSync(page, data);
|
||||||
for (let y = 0; y < 16; y++) {
|
for (let y = 0; y < 16; y++) {
|
||||||
for (let x = 0; x < 16; x++) {
|
for (let x = 0; x < 16; x++) {
|
||||||
const i = y * 16 + x;
|
const i = y * 16 + x;
|
||||||
@@ -564,11 +689,13 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('minimumContrastRatio', async () => {
|
describe('minimumContrastRatio', async () => {
|
||||||
before(async () => setupBrowser());
|
if (areTestsEnabled) {
|
||||||
after(async () => browser.close());
|
before(async () => setupBrowser());
|
||||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
after(async () => browser.close());
|
||||||
|
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||||
|
}
|
||||||
|
|
||||||
it('should adjust 0-15 colors on black background', async () => {
|
itWebgl('should adjust 0-15 colors on black background', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
black: '#2e3436',
|
black: '#2e3436',
|
||||||
red: '#cc0000',
|
red: '#cc0000',
|
||||||
@@ -591,7 +718,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||||
window.term.setOption('minimumContrastRatio', 1);
|
window.term.setOption('minimumContrastRatio', 1);
|
||||||
`);
|
`);
|
||||||
await writeSync(
|
await writeSync(page,
|
||||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||||
);
|
);
|
||||||
@@ -635,7 +762,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
|
await pollFor(page, () => getCellColor(8, 2), [0xee, 0xee, 0xec, 255]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should adjust 0-15 colors on white background', async () => {
|
itWebgl('should adjust 0-15 colors on white background', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
background: '#ffffff',
|
background: '#ffffff',
|
||||||
black: '#2e3436',
|
black: '#2e3436',
|
||||||
@@ -659,7 +786,7 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
window.term.setOption('theme', ${JSON.stringify(theme)});
|
window.term.setOption('theme', ${JSON.stringify(theme)});
|
||||||
window.term.setOption('minimumContrastRatio', 1);
|
window.term.setOption('minimumContrastRatio', 1);
|
||||||
`);
|
`);
|
||||||
await writeSync(
|
await writeSync(page,
|
||||||
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
`\\x1b[30m█\\x1b[31m█\\x1b[32m█\\x1b[33m█\\x1b[34m█\\x1b[35m█\\x1b[36m█\\x1b[37m█\\r\\n` +
|
||||||
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
`\\x1b[90m█\\x1b[91m█\\x1b[92m█\\x1b[93m█\\x1b[94m█\\x1b[95m█\\x1b[96m█\\x1b[97m█`
|
||||||
);
|
);
|
||||||
@@ -703,37 +830,52 @@ describe('WebGL Renderer Integration Tests', function(): void {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('selection', async () => {
|
||||||
|
if (areTestsEnabled) {
|
||||||
|
before(async () => setupBrowser());
|
||||||
|
after(async () => browser.close());
|
||||||
|
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||||
|
}
|
||||||
|
|
||||||
|
itWebgl('should resolve the inverse foreground color based on the original background color, not the selection', async () => {
|
||||||
|
const theme: ITheme = {
|
||||||
|
foreground: '#FF0000',
|
||||||
|
background: '#00FF00',
|
||||||
|
selection: '#0000FF'
|
||||||
|
};
|
||||||
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
|
await writeSync(page, ` █\\x1b[7m█\\x1b[0m`);
|
||||||
|
await pollFor(page, () => getCellColor(1, 1), [0, 255, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]);
|
||||||
|
await page.evaluate(`window.term.selectAll()`);
|
||||||
|
// Selection only cell needs to be first to ensure renderer has kicked in
|
||||||
|
await pollFor(page, () => getCellColor(1, 1), [0, 0, 255, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(2, 1), [255, 0, 0, 255]);
|
||||||
|
await pollFor(page, () => getCellColor(3, 1), [0, 255, 0, 255]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('allowTransparency', async () => {
|
describe('allowTransparency', async () => {
|
||||||
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true}));
|
if (areTestsEnabled) {
|
||||||
after(async () => browser.close());
|
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true }));
|
||||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
after(async () => browser.close());
|
||||||
it('transparent background inverse', async () => {
|
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||||
|
}
|
||||||
|
|
||||||
|
itWebgl('transparent background inverse', async () => {
|
||||||
const theme: ITheme = {
|
const theme: ITheme = {
|
||||||
background: '#ff000080'
|
background: '#ff000080'
|
||||||
};
|
};
|
||||||
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
await page.evaluate(`window.term.setOption('theme', ${JSON.stringify(theme)});`);
|
||||||
const data = `\\x1b[7m█\x1b[0m`;
|
const data = `\\x1b[7m█\x1b[0m`;
|
||||||
await writeSync(data);
|
await writeSync(page, data);
|
||||||
// Inverse background should be opaque
|
// Inverse background should be opaque
|
||||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function openTerminal(options: ITerminalOptions = {}): Promise<void> {
|
|
||||||
await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
|
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
|
||||||
if (options.rendererType === 'dom') {
|
|
||||||
await page.waitForSelector('.xterm-rows');
|
|
||||||
} else {
|
|
||||||
await page.waitForSelector('.xterm-text-layer');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeSync(data: string): Promise<void> {
|
|
||||||
return page.evaluate(`new Promise(resolve => window.term.write('${data}', resolve))`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCellColor(col: number, row: number): Promise<number[]> {
|
async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||||
await page.evaluate(`
|
await page.evaluate(`
|
||||||
window.gl = window.term._core._renderService._renderer._gl;
|
window.gl = window.term._core._renderService._renderer._gl;
|
||||||
@@ -749,32 +891,20 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
|
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
|
||||||
browser = await puppeteer.launch({
|
const browserType = getBrowserType();
|
||||||
headless: process.argv.indexOf('--headless') !== -1,
|
browser = await browserType.launch({
|
||||||
args: [`--window-size=${width},${height}`, `--no-sandbox`]
|
headless: process.argv.includes('--headless')
|
||||||
});
|
});
|
||||||
page = (await browser.pages())[0];
|
page = await (await browser.newContext()).newPage();
|
||||||
await page.setViewport({ width, height });
|
await page.setViewportSize({ width, height });
|
||||||
await page.goto(APP);
|
await page.goto(APP);
|
||||||
await openTerminal(options);
|
await openTerminal(page, options);
|
||||||
await page.evaluate(`
|
await page.evaluate(`
|
||||||
window.addon = new WebglAddon(true);
|
window.addon = new WebglAddon(true);
|
||||||
window.term.loadAddon(window.addon);
|
window.term.loadAddon(window.addon);
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function pollFor<T>(page: puppeteer.Page, evalOrFn: string | (() => Promise<T>), val: T, preFn?: () => Promise<void>): Promise<void> {
|
|
||||||
if (preFn) {
|
|
||||||
await preFn();
|
|
||||||
}
|
|
||||||
const result = typeof evalOrFn === 'string' ? await page.evaluate(evalOrFn) : await evalOrFn();
|
|
||||||
if (!deepEqual(result, val)) {
|
|
||||||
return new Promise<void>(r => {
|
|
||||||
setTimeout(() => r(pollFor(page, evalOrFn, val, preFn)), 1);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const COLORS_16_TO_255 = [
|
const COLORS_16_TO_255 = [
|
||||||
'#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
|
'#000000', '#00005f', '#000087', '#0000af', '#0000d7', '#0000ff', '#005f00', '#005f5f', '#005f87', '#005faf', '#005fd7', '#005fff', '#008700', '#00875f', '#008787', '#0087af',
|
||||||
'#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
|
'#0087d7', '#0087ff', '#00af00', '#00af5f', '#00af87', '#00afaf', '#00afd7', '#00afff', '#00d700', '#00d75f', '#00d787', '#00d7af', '#00d7d7', '#00d7ff', '#00ff00', '#00ff5f',
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user