Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac923e55d2 | ||
|
|
e08e5607ed | ||
|
|
2ac2eeadfc | ||
|
|
ffadc0b31d | ||
|
|
cc1089e943 | ||
|
|
47c21df83d | ||
|
|
380e6805e8 | ||
|
|
9f424833d6 | ||
|
|
90ccc815cb | ||
|
|
8de7bd5850 | ||
|
|
9fed8e2a3a | ||
|
|
118915c1f7 | ||
|
|
fca593d93b | ||
|
|
3376ee2f85 | ||
|
|
f6fbf63673 | ||
|
|
3e5c6a156b | ||
|
|
a91665cb18 | ||
|
|
22140b5e94 | ||
|
|
22a4dc4b6b | ||
|
|
0ff06db417 | ||
|
|
83a77b6024 | ||
|
|
8b422442b7 | ||
|
|
02ce3eff87 | ||
|
|
518b87f01d | ||
|
|
6188c07bf7 | ||
|
|
325d133a75 | ||
|
|
b662824b81 | ||
|
|
ada5bfe048 | ||
|
|
f270127ce5 | ||
|
|
7460782494 | ||
|
|
048010c219 | ||
|
|
cdec1e5b03 | ||
|
|
eb56356133 | ||
|
|
2bfd95fe54 | ||
|
|
e9b0593298 | ||
|
|
d5c47ec531 | ||
|
|
5a145fcffe | ||
|
|
539fed4daa | ||
|
|
a0493a604c | ||
|
|
07e8f86bd1 | ||
|
|
1dbc1bf639 | ||
|
|
6e8c59f0db | ||
|
|
ec227c757b | ||
|
|
4fbeb58203 | ||
|
|
60f549028f | ||
|
|
cc5136aeb3 | ||
|
|
a1e9105ce7 | ||
|
|
7a7f4f0add | ||
|
|
96ea9fe188 | ||
|
|
b4e2293dbc | ||
|
|
7194223696 | ||
|
|
8c9e44c219 | ||
|
|
075f33031d | ||
|
|
e4b670231d | ||
|
|
1c19f1d282 | ||
|
|
508589aa80 | ||
|
|
854e2736f6 | ||
|
|
58f650e6f0 | ||
|
|
913cb25863 | ||
|
|
16f82cb3b5 | ||
|
|
0a101a01f5 | ||
|
|
9638063359 | ||
|
|
ee26780e03 | ||
|
|
b35415ed16 | ||
|
|
2a9ec8fd1d | ||
|
|
2fdb46919c | ||
|
|
c8e633ecb5 | ||
|
|
c546f7cc8a | ||
|
|
6997f1a940 | ||
|
|
14c6df3187 | ||
|
|
18be346df9 | ||
|
|
37297e09ed | ||
|
|
b2f0cc8dc1 | ||
|
|
6cbee3349e | ||
|
|
f0edc1b613 | ||
|
|
cd9867e222 | ||
|
|
e1d852c86a | ||
|
|
455c744ab1 | ||
|
|
21f48ba803 | ||
|
|
49fdacba6a | ||
|
|
a359c7ca10 | ||
|
|
242eade757 | ||
|
|
4037b943b9 | ||
|
|
9154831afa | ||
|
|
d6599bc048 | ||
|
|
8a1faed5cb | ||
|
|
72a157df5e | ||
|
|
6daadc4c55 | ||
|
|
c05d0f650c | ||
|
|
d5b5f1a9a4 | ||
|
|
dfcd674f8c | ||
|
|
a7e0a9c534 | ||
|
|
5839accbf1 | ||
|
|
01c9a98e8e | ||
|
|
bcfed71e1d | ||
|
|
046d4b8314 | ||
|
|
a9da61e605 | ||
|
|
24c7be210c | ||
|
|
9c815250b6 | ||
|
|
377f88796f | ||
|
|
ed6c849501 | ||
|
|
5a21948b4a | ||
|
|
b24bda92ad | ||
|
|
9895691e0e | ||
|
|
77870b06b9 | ||
|
|
41dc93a008 | ||
|
|
ba9f2e0f11 | ||
|
|
b3fb8db6b3 | ||
|
|
91946dcd9b | ||
|
|
41c0e26292 | ||
|
|
fe89817667 | ||
|
|
7ee7aa2719 | ||
|
|
b05c26c6fd | ||
|
|
0adbedf98d | ||
|
|
89ead0b4b7 | ||
|
|
349510065b | ||
|
|
fa8a0e7f24 | ||
|
|
0c4e9c6b27 | ||
|
|
de2b62eeac | ||
|
|
65b2af10f8 | ||
|
|
1a20fbb25f | ||
|
|
d51ba2578b | ||
|
|
56734874ac | ||
|
|
72f1d1048a | ||
|
|
d17df65fad | ||
|
|
c2504fa669 | ||
|
|
5a2bf6d039 | ||
|
|
568e4c6935 | ||
|
|
8b1c5c4ffb | ||
|
|
d34bb5ed79 | ||
|
|
ace4c3663f | ||
|
|
75075ebb70 | ||
|
|
7b8ad8c0b4 | ||
|
|
f28c21eb77 | ||
|
|
3142460db5 | ||
|
|
b97b1b6093 | ||
|
|
303db85763 | ||
|
|
49ec7199e9 | ||
|
|
19c760a204 | ||
|
|
3a81ef54ca | ||
|
|
6cfdcb1b0d | ||
|
|
30ef1fcaa5 | ||
|
|
84b8121105 | ||
|
|
218cfd256c | ||
|
|
1757f2733c | ||
|
|
9411acbc73 | ||
|
|
cb4102cdcb | ||
|
|
1926c9bbbc | ||
|
|
06145c02b1 | ||
|
|
d65898ffed | ||
|
|
b5e6066f8c | ||
|
|
0981ca8796 | ||
|
|
ffeaa2b976 | ||
|
|
167765f932 | ||
|
|
bcdbb61ed4 | ||
|
|
ea7571c2b9 | ||
|
|
880b726bcb | ||
|
|
902da2800b | ||
|
|
2bd9c4e839 | ||
|
|
b3d36478ca | ||
|
|
b07c016fea | ||
|
|
0b5625581e | ||
|
|
e4b473785d | ||
|
|
cb8d93a1ef | ||
|
|
8636d0f820 | ||
|
|
f79644bd4c | ||
|
|
917e31ee17 | ||
|
|
aba59e9386 | ||
|
|
1c7c4eb62f | ||
|
|
fc41968131 | ||
|
|
04177de95b | ||
|
|
c9c07aeabd | ||
|
|
0cc759e0b1 | ||
|
|
852255ea03 | ||
|
|
88f459583e | ||
|
|
fd8a0690c9 | ||
|
|
6e2744a299 | ||
|
|
15e3b1ac12 | ||
|
|
5a5ed6d767 | ||
|
|
74ea0c23ca | ||
|
|
6d0a287ed9 | ||
|
|
83ff4e97c3 | ||
|
|
7b28dc4d59 | ||
|
|
02d3ef599a | ||
|
|
c118f04187 | ||
|
|
2bd3869547 | ||
|
|
69379ba59b | ||
|
|
5ed05295ba | ||
|
|
4ee4a0f988 | ||
|
|
d6b8a21219 | ||
|
|
2e2d275d6c | ||
|
|
33bba35be7 | ||
|
|
c7660fb49c | ||
|
|
6d527c42b0 | ||
|
|
440da4a5ff | ||
|
|
e66fd50383 | ||
|
|
f69f9decca | ||
|
|
e70138b63c | ||
|
|
becec91e98 | ||
|
|
03dc1908a5 | ||
|
|
d2298d84a0 | ||
|
|
0b56b56c06 | ||
|
|
6d46b7b86f | ||
|
|
b829ad8386 | ||
|
|
9bc65c36a6 | ||
|
|
a562f33d53 | ||
|
|
fe30cd6ed8 | ||
|
|
9e84895141 | ||
|
|
497df06f3f | ||
|
|
650cda31fd | ||
|
|
dec3791f4c | ||
|
|
e46a9e12d0 | ||
|
|
7f64fa9ddd | ||
|
|
73d20c1d3b | ||
|
|
fd44d76262 | ||
|
|
309ba47483 | ||
|
|
4084dc3949 | ||
|
|
42bacc3283 | ||
|
|
46bd97247d | ||
|
|
3bf72cdefc | ||
|
|
de23616589 | ||
|
|
39c2e57ad3 | ||
|
|
d19361ac85 | ||
|
|
1f5159febe | ||
|
|
3a2ae96960 | ||
|
|
ad1f2ac2a8 | ||
|
|
b972e7f5ad | ||
|
|
503fe99b0a | ||
|
|
953cf005c0 | ||
|
|
d177acccee | ||
|
|
4451369c66 | ||
|
|
6b09c1b576 | ||
|
|
71eb514bd0 | ||
|
|
cd552500fc | ||
|
|
ed1773c103 | ||
|
|
b7c119a071 | ||
|
|
452241ca92 | ||
|
|
1880399849 | ||
|
|
d4ee0309d9 | ||
|
|
a351f5758a | ||
|
|
3597d43788 | ||
|
|
f381783bd5 | ||
|
|
6969a94fe4 | ||
|
|
2377472eca | ||
|
|
0edabed595 | ||
|
|
7d98375ce4 | ||
|
|
862e83d7be | ||
|
|
7cf9b68e2c | ||
|
|
97a0df260b | ||
|
|
c66dc82286 | ||
|
|
6c29864847 | ||
|
|
d82da195f3 | ||
|
|
abfe391d91 | ||
|
|
c22e684904 | ||
|
|
6406fd79c1 | ||
|
|
0271fdf49d | ||
|
|
805b41b80f | ||
|
|
c9bacc80d7 | ||
|
|
54ee698ba6 | ||
|
|
295e3c7f0c | ||
|
|
d219da480c | ||
|
|
02fa38c334 | ||
|
|
4d05905736 | ||
|
|
81a3c80bc4 | ||
|
|
f6fabebf8d | ||
|
|
b735e7f1c4 | ||
|
|
839271096e | ||
|
|
6a087e9896 | ||
|
|
dda3f5f545 | ||
|
|
04041d030f | ||
|
|
095ceda639 | ||
|
|
0aa9fc5bb8 | ||
|
|
e092ca70e1 | ||
|
|
0592837620 | ||
|
|
fc03a1698b | ||
|
|
c9ef4a8eb9 | ||
|
|
846573bb01 | ||
|
|
0f70055d76 | ||
|
|
3f2ed42e73 | ||
|
|
2a3c2d9520 | ||
|
|
5e95475d6b | ||
|
|
57a65e0858 | ||
|
|
bfa9dd5060 | ||
|
|
04f79aa7b4 | ||
|
|
86ccd709bf | ||
|
|
c257a7247e | ||
|
|
0a49a5dde1 | ||
|
|
c6e3cc007c | ||
|
|
713baeb64d | ||
|
|
efba2c9040 | ||
|
|
67ef455515 | ||
|
|
975d52fab9 | ||
|
|
6b37187b4d | ||
|
|
84ab6a49c4 | ||
|
|
4ebc75aae0 | ||
|
|
54335ef54a | ||
|
|
43d8db77f9 | ||
|
|
5a4a46f130 | ||
|
|
b0672aac09 | ||
|
|
fcf378b172 | ||
|
|
25551a4b98 | ||
|
|
67b3faef01 | ||
|
|
8b545a3fe4 | ||
|
|
4f4ce0146d | ||
|
|
e7b347a2ef | ||
|
|
7908b7cf62 | ||
|
|
392f8e9c98 | ||
|
|
b5cd4f1492 | ||
|
|
4edfc12cbf | ||
|
|
19c1ee56d3 | ||
|
|
a4f8b07a7f | ||
|
|
8d4d5bd54b | ||
|
|
d5710afcb3 | ||
|
|
8bd8195e30 | ||
|
|
ffe066dfef | ||
|
|
11e90591f9 | ||
|
|
0b84d17b18 | ||
|
|
376f0d9972 | ||
|
|
478535d236 | ||
|
|
3f12d2925a | ||
|
|
8e65ee8832 | ||
|
|
a7f0c3868c | ||
|
|
96486c414b | ||
|
|
b560d85553 | ||
|
|
7796b4101a | ||
|
|
a982484392 | ||
|
|
e15c6b9f3e | ||
|
|
807183bba6 | ||
|
|
dc19628014 | ||
|
|
ca68a5c59a | ||
|
|
22e8a2f8e7 | ||
|
|
171ba446bd | ||
|
|
89e0b87eac | ||
|
|
2c9e711cae | ||
|
|
b0f1c38fd2 | ||
|
|
b393008b54 | ||
|
|
65f41a151e | ||
|
|
ddc4e75583 | ||
|
|
869ce3aa35 | ||
|
|
16ea621a5f | ||
|
|
5204374494 | ||
|
|
9f1a71485c | ||
|
|
49e0480f78 | ||
|
|
114924f3fa | ||
|
|
aeba140c75 | ||
|
|
aea7680f76 | ||
|
|
fb4a4e6127 | ||
|
|
cc82f605fe | ||
|
|
a055948937 | ||
|
|
8246440795 | ||
|
|
073aa6d439 | ||
|
|
6bbe5a82a8 | ||
|
|
1e6dfec4ae | ||
|
|
5285b9ac45 | ||
|
|
50d1834871 | ||
|
|
b1f785879b | ||
|
|
28fcffe46c | ||
|
|
14b99dca76 | ||
|
|
1b13127f0b | ||
|
|
a37ea14d10 | ||
|
|
f8a5d3a91e | ||
|
|
8d7137417b | ||
|
|
e47296c1b9 | ||
|
|
96b26b2e4a | ||
|
|
8ac88bc6bf | ||
|
|
7eead44048 | ||
|
|
49a0904857 | ||
|
|
a4c62792df | ||
|
|
1c345bef48 | ||
|
|
415100e8b8 | ||
|
|
627410a537 | ||
|
|
cad9c4164c | ||
|
|
a87b6c3f2e | ||
|
|
a2a855d747 | ||
|
|
fb91b01217 | ||
|
|
bd661fdb07 | ||
|
|
f983b3cac1 | ||
|
|
0ba12301a6 | ||
|
|
97e084aa50 | ||
|
|
e0fb3b4d19 | ||
|
|
a1050d1a11 | ||
|
|
7f9e3b1225 | ||
|
|
48f15cb543 | ||
|
|
6e35bcd767 | ||
|
|
80e474c6ad | ||
|
|
4dee0c5d4b | ||
|
|
a22ad76bd8 | ||
|
|
4f5154fd21 | ||
|
|
cc6def57e4 | ||
|
|
c4750eaf77 | ||
|
|
8db2244f5d | ||
|
|
5d8e4a0af4 | ||
|
|
eecf52e949 | ||
|
|
d8c31d7ad0 | ||
|
|
d8cf3af555 | ||
|
|
d3976c05f1 | ||
|
|
94c3d11239 | ||
|
|
00f6d3a88c | ||
|
|
f61d8c91a3 | ||
|
|
ac659cb412 | ||
|
|
34475f24c3 | ||
|
|
8116e035b4 | ||
|
|
56b37a73ac | ||
|
|
fc8043a25a | ||
|
|
f88c0e7480 | ||
|
|
d948b31304 | ||
|
|
9a4d5dbe26 | ||
|
|
8e7093f03b | ||
|
|
c4eef2f470 | ||
|
|
5fcb084089 | ||
|
|
e89c507d06 | ||
|
|
eb3de3ebd5 | ||
|
|
ffa4d921bb | ||
|
|
a060c64a08 | ||
|
|
0d5a526e86 | ||
|
|
3ec08948ec | ||
|
|
4d97729010 | ||
|
|
5f1651ffbc | ||
|
|
ab0082b73a | ||
|
|
aa55d1bde6 | ||
|
|
e0324e32a8 | ||
|
|
3b260a2193 | ||
|
|
622acaaad7 | ||
|
|
0d7e18d236 | ||
|
|
83c09c34db | ||
|
|
03e3105c55 | ||
|
|
c5d015c908 | ||
|
|
e1e8f28d97 | ||
|
|
f6a61acc86 | ||
|
|
6316eee672 | ||
|
|
9259ec258f | ||
|
|
c741c13409 | ||
|
|
f2ff3a6a7a | ||
|
|
dbef850039 | ||
|
|
590a8219e3 | ||
|
|
d22f7c9edd | ||
|
|
c2bf87ee34 | ||
|
|
0bc5ca8f80 | ||
|
|
5b785d29a1 | ||
|
|
f38a49b4d5 | ||
|
|
26c974f9b6 | ||
|
|
333b0e28ac | ||
|
|
0bc71156bf | ||
|
|
dece3cc41c | ||
|
|
8752c4fa8e | ||
|
|
53d0297898 | ||
|
|
af6f03600f | ||
|
|
1f8e6f0952 | ||
|
|
f087bd121e | ||
|
|
d669c6e9d6 | ||
|
|
c5f9eacbb1 | ||
|
|
6fcc496547 | ||
|
|
5dee66353a | ||
|
|
2a602ef231 | ||
|
|
e3e3c347ea | ||
|
|
2e8b793ba0 | ||
|
|
03fcf60aaf | ||
|
|
ed15a8e170 | ||
|
|
969db2bcf9 | ||
|
|
8e378c07e9 | ||
|
|
1a22c44f2b | ||
|
|
cf9949840b | ||
|
|
3a90f7e7c0 | ||
|
|
035025dba3 | ||
|
|
52ed65d7bd | ||
|
|
150499b7e5 | ||
|
|
193d305dcd | ||
|
|
2236e230fc | ||
|
|
cd6f370c28 | ||
|
|
2659de2291 | ||
|
|
34df17e36d | ||
|
|
44388e5e6c | ||
|
|
785b932d75 | ||
|
|
e58f73dc82 | ||
|
|
7bba07e24e | ||
|
|
39eea8ed88 | ||
|
|
20c1f1a5f7 | ||
|
|
e87f70d6d6 | ||
|
|
51c580e7a4 | ||
|
|
23328a394b | ||
|
|
399621eeb5 | ||
|
|
66675ef649 | ||
|
|
c6df208a3e | ||
|
|
8201925fe8 | ||
|
|
d74e43a42a | ||
|
|
25b69e5ed0 | ||
|
|
37c722d556 | ||
|
|
2b597a82a1 | ||
|
|
318abe641b | ||
|
|
eca9c721d4 | ||
|
|
0b8120808c | ||
|
|
c6e4788344 | ||
|
|
c548977a3e | ||
|
|
04d196a32d | ||
|
|
494fe3a95d | ||
|
|
8f8813a19a | ||
|
|
df8a57d14b | ||
|
|
868fc7ec29 | ||
|
|
2febe19222 | ||
|
|
693d09cbad | ||
|
|
ab90dcd0dc | ||
|
|
1f45d5344c | ||
|
|
a9eea3fd46 | ||
|
|
aec1f2b2be | ||
|
|
66efb0388a | ||
|
|
4ac5a91c95 | ||
|
|
4e8e6b9788 | ||
|
|
275a8a91ee | ||
|
|
270ac1c7d9 | ||
|
|
c42f290eb8 | ||
|
|
8b323bbf69 | ||
|
|
e68bf3ca5b | ||
|
|
b742da4bfa | ||
|
|
6368cb4cff | ||
|
|
81d68b115c | ||
|
|
d62b4531c3 | ||
|
|
1431210ecd | ||
|
|
50b1e1045c | ||
|
|
09a5da5d33 | ||
|
|
349acbba34 | ||
|
|
78128762f9 | ||
|
|
497cfcb74c | ||
|
|
11c806a4d0 | ||
|
|
9fb84cb89d | ||
|
|
1ceec0b6fa | ||
|
|
bbff3e8396 | ||
|
|
73cb96ddec | ||
|
|
904ddffc8d | ||
|
|
c23f4313fb | ||
|
|
25eb7d6472 | ||
|
|
6225cc2b9c | ||
|
|
d3fd3d0f22 | ||
|
|
3387d7fe46 | ||
|
|
74b5d3f881 | ||
|
|
822ac527ba | ||
|
|
2032f409cf | ||
|
|
f3622dfedc | ||
|
|
a39a468f19 | ||
|
|
2935d9feb7 | ||
|
|
21105b4017 | ||
|
|
b01a94f5bb | ||
|
|
c57a52b900 | ||
|
|
3953db0d68 | ||
|
|
1923473a73 | ||
|
|
d739191adb | ||
|
|
9ef2397918 | ||
|
|
e9d276a827 | ||
|
|
327ece07e8 | ||
|
|
4752f99e03 | ||
|
|
e5ad3ce9b9 | ||
|
|
faf5f72c1f | ||
|
|
e72e575c0f | ||
|
|
318b60418e | ||
|
|
68fe100841 | ||
|
|
d511720f63 | ||
|
|
2608db2728 | ||
|
|
fc46d69a57 | ||
|
|
74fc7e2686 | ||
|
|
f08ad87e7a | ||
|
|
65f0f9a1d4 | ||
|
|
93bc9bf152 | ||
|
|
e8e2f16cff | ||
|
|
1b38a004a3 | ||
|
|
df1d72aee5 | ||
|
|
bb4fbf2659 | ||
|
|
f9db65c52e | ||
|
|
f816ed0f71 | ||
|
|
64c8c16730 | ||
|
|
70d30f5a09 | ||
|
|
edc4fb4a29 | ||
|
|
ef1b773fd7 | ||
|
|
06d6e08347 | ||
|
|
3acf08bc0c | ||
|
|
ab8ce61e56 | ||
|
|
c5a6b997d1 | ||
|
|
6d2d4555e7 | ||
|
|
d673e02afe | ||
|
|
151a5d88ce | ||
|
|
f309affff2 | ||
|
|
9459553665 | ||
|
|
4a6e7bee21 | ||
|
|
ef759a6d7b | ||
|
|
9dca3c48c7 | ||
|
|
284d9f9893 | ||
|
|
99c47eb3c3 | ||
|
|
c5861d512c | ||
|
|
22cca3495a | ||
|
|
0c864cddba | ||
|
|
3f3fc183f4 | ||
|
|
cad6b49e39 | ||
|
|
7dae4bc8bc | ||
|
|
5c6d16ef0a | ||
|
|
f7e0f69598 | ||
|
|
591f178d31 | ||
|
|
3b4ecd0709 | ||
|
|
4a50ca81c6 | ||
|
|
4effa381f7 | ||
|
|
caaca169eb | ||
|
|
1d945fdffc | ||
|
|
23ee719f6e | ||
|
|
eb31850e87 | ||
|
|
1b64a09f50 | ||
|
|
823f7345b0 | ||
|
|
0727741ca1 | ||
|
|
5e3f63e876 | ||
|
|
0237b2965e | ||
|
|
3105797da1 | ||
|
|
4d837e2802 | ||
|
|
4ecc098de4 | ||
|
|
e76918eddc | ||
|
|
903830ce1f | ||
|
|
2526409ca5 | ||
|
|
7ffd9e1f7f | ||
|
|
4979e9d2be | ||
|
|
8bbbc98ac8 | ||
|
|
98ceafb57a | ||
|
|
f6860442fb | ||
|
|
4382a551d1 | ||
|
|
774ebe40e6 | ||
|
|
6e3388a2c4 | ||
|
|
d92d7904ab | ||
|
|
523de80162 | ||
|
|
e40cf0684e | ||
|
|
b68f82078b | ||
|
|
86ba6cd288 | ||
|
|
ac81d3f72d | ||
|
|
559b6950b2 | ||
|
|
b59de22c36 | ||
|
|
86c8ff0e33 | ||
|
|
c0ef2ccb5a | ||
|
|
0698afaf12 | ||
|
|
79d47893df | ||
|
|
7e6282e9c2 | ||
|
|
9a119a7292 | ||
|
|
45b019bd89 | ||
|
|
81a62306af | ||
|
|
5312602692 | ||
|
|
413a885b5e | ||
|
|
bc528cd870 | ||
|
|
2cfbee413f | ||
|
|
e87e15e87b | ||
|
|
8191483fbd | ||
|
|
9c9ff2b38c | ||
|
|
c39d235168 | ||
|
|
d9b8c6a838 | ||
|
|
ace99d125d | ||
|
|
fd79100d95 | ||
|
|
7970b5c1e3 | ||
|
|
91d46dae50 | ||
|
|
6e123e90ee | ||
|
|
a1385f589b | ||
|
|
4f46f8e123 | ||
|
|
c021571194 | ||
|
|
f0926e7821 | ||
|
|
d495fdd9ab | ||
|
|
f3f6a91f16 | ||
|
|
409cb4bb73 | ||
|
|
d608912efd | ||
|
|
9d7cef3d63 | ||
|
|
bc57ea3a7a | ||
|
|
daba61bd35 | ||
|
|
76d9d5a324 | ||
|
|
830f8f3231 | ||
|
|
04eb6a4160 | ||
|
|
3f1480a52a | ||
|
|
14b02b9ad5 | ||
|
|
f3cd6134f1 | ||
|
|
9261780a1d | ||
|
|
a434800303 | ||
|
|
39959c2861 | ||
|
|
03c75360b1 | ||
|
|
85984681bb | ||
|
|
0910e58b15 | ||
|
|
14d59999e4 | ||
|
|
8f6bde9641 | ||
|
|
436f8556fd | ||
|
|
face97d012 | ||
|
|
8d92f91029 | ||
|
|
1ede106ab4 | ||
|
|
e7e71dbd3c | ||
|
|
208364147a | ||
|
|
d6fe5c7384 | ||
|
|
75ca5006a2 | ||
|
|
266d795179 | ||
|
|
d0571d8634 | ||
|
|
ca10ef5cad | ||
|
|
31686fda61 | ||
|
|
0a83040ac8 | ||
|
|
889e3ad14b | ||
|
|
8a596b220a | ||
|
|
8d57c64d29 | ||
|
|
bbd03fb5fe | ||
|
|
bff529e1e2 | ||
|
|
f4618b229f | ||
|
|
686105942c | ||
|
|
0514b55936 | ||
|
|
04a14a748e | ||
|
|
ed492f7253 | ||
|
|
d459e520d3 | ||
|
|
ce4a85d07d | ||
|
|
309487a020 | ||
|
|
c7122e8447 | ||
|
|
027fffbadc | ||
|
|
dc30a51017 | ||
|
|
640596b043 | ||
|
|
d3b3357f3f | ||
|
|
f0ddaf49ea | ||
|
|
d80a47f539 | ||
|
|
d0ec9f2591 | ||
|
|
941cc5100d | ||
|
|
e0d4384d3c | ||
|
|
f45eb02394 | ||
|
|
db79efdbc1 | ||
|
|
8afa714276 | ||
|
|
0befb9c251 | ||
|
|
a8f63c478c | ||
|
|
ff33834eb2 | ||
|
|
a2e07de90c | ||
|
|
b0c29cac9a | ||
|
|
7f58c48503 | ||
|
|
eb93b02c6c | ||
|
|
11affae761 | ||
|
|
53e4e9ad72 | ||
|
|
4ac8e4ca51 | ||
|
|
954d9371a1 | ||
|
|
6493679b2e | ||
|
|
1af6175e48 | ||
|
|
906a6fc558 | ||
|
|
9cb25fbf06 | ||
|
|
48142eca23 | ||
|
|
1fe0ec8d2c | ||
|
|
f8a9eb1db6 | ||
|
|
df14ce49d7 | ||
|
|
7f5ed40db1 | ||
|
|
29aca47e0d | ||
|
|
055bcce18d | ||
|
|
006249ce65 | ||
|
|
60f9bf4398 | ||
|
|
848aceaa25 | ||
|
|
9e650cea9f | ||
|
|
de78344165 | ||
|
|
2862832f6f | ||
|
|
8df878b8a9 | ||
|
|
0e07e419a2 | ||
|
|
04b513cf9b | ||
|
|
f310f118b4 | ||
|
|
8d308a7309 | ||
|
|
d9880c0270 | ||
|
|
f6940aa201 | ||
|
|
2482c0e8d3 | ||
|
|
1be14e4338 | ||
|
|
51f89bbe4d | ||
|
|
0b0662c4d9 | ||
|
|
9cbd9a0456 | ||
|
|
770de8d05b | ||
|
|
4372d8ccca | ||
|
|
0b81af1811 | ||
|
|
ed92cc0c3f | ||
|
|
8b276c1a54 | ||
|
|
d2be5f992a | ||
|
|
437c8aac5c | ||
|
|
84b259dfa5 | ||
|
|
a57b69d5de | ||
|
|
8f0f33fd6d | ||
|
|
4d542d23c6 | ||
|
|
4637983888 | ||
|
|
f1ff42410e | ||
|
|
1ef9f7c1d1 | ||
|
|
8901740113 | ||
|
|
2fffe9fc89 | ||
|
|
423d5402f7 | ||
|
|
2b2d6c5fb3 | ||
|
|
c55075b666 | ||
|
|
09df783959 | ||
|
|
3bdbfb6d5f | ||
|
|
520a270afc | ||
|
|
d1f53e2b02 | ||
|
|
940c93e9cd | ||
|
|
1a5c881c7c | ||
|
|
fb3886a9ae | ||
|
|
3271b45681 | ||
|
|
43f38cddb1 | ||
|
|
e5037a6d1e | ||
|
|
119b64181b | ||
|
|
c52eb8898f | ||
|
|
ced5c304f1 | ||
|
|
b3cffe2d30 | ||
|
|
73b3e31b38 | ||
|
|
cbddd2029e | ||
|
|
48cfd23219 | ||
|
|
1c6ce99419 | ||
|
|
ab7da06cc1 | ||
|
|
440ab765c4 | ||
|
|
e7dbdb1362 | ||
|
|
8c16d9d506 | ||
|
|
71911b501b | ||
|
|
b2aa100c58 | ||
|
|
6d932ec812 | ||
|
|
5a658ebfcc | ||
|
|
7bd3801f58 | ||
|
|
8cfa7a0ee2 | ||
|
|
140073675c | ||
|
|
238a2bafb1 | ||
|
|
09672feca0 | ||
|
|
7a17a26e18 | ||
|
|
fafa7efd5e | ||
|
|
841cd636ca | ||
|
|
ceeb8585d7 | ||
|
|
dc4940e26e | ||
|
|
8a19f8c1cc | ||
|
|
66d6ed65ef | ||
|
|
09ae3a9b44 | ||
|
|
78bfe0b89d | ||
|
|
4b8e7a5768 | ||
|
|
fb4cb0f140 | ||
|
|
a55728dd28 | ||
|
|
52dc6a456c | ||
|
|
078b83fcaf | ||
|
|
c895f67fd8 | ||
|
|
9159fc68e1 | ||
|
|
5664ff3101 | ||
|
|
78ddb4b14a | ||
|
|
4744ef34b2 | ||
|
|
f4bc9f1ecd | ||
|
|
b11217aa36 | ||
|
|
34fe6e2260 | ||
|
|
160e9ee35b | ||
|
|
d98198e1b9 | ||
|
|
493975b41e | ||
|
|
3322418728 | ||
|
|
2ceec34809 | ||
|
|
8c55c83e4a | ||
|
|
9acc36f8c0 | ||
|
|
a4f7e2d8e9 | ||
|
|
ee2e54487d | ||
|
|
f4ffb13ece | ||
|
|
a39c7fb636 | ||
|
|
0ccf3beec7 | ||
|
|
90b6f058c9 | ||
|
|
0165c3612d | ||
|
|
47b1fdd147 | ||
|
|
ad8404417e | ||
|
|
3d6c25bf2d | ||
|
|
e25471361f | ||
|
|
cb10aadb06 | ||
|
|
89f5c2e06c | ||
|
|
dbe2b98bb4 | ||
|
|
4087284822 | ||
|
|
759a5928a2 | ||
|
|
43bbea8eb5 | ||
|
|
fd8fc18302 | ||
|
|
f34a30b24f | ||
|
|
91a362be7b | ||
|
|
19fb806214 | ||
|
|
70b5e0249c | ||
|
|
d9703aa7fe | ||
|
|
03f15a7fb7 | ||
|
|
8cd01dc421 | ||
|
|
1f89223c2a | ||
|
|
8287e783a2 | ||
|
|
8184e0824b | ||
|
|
72d18f7d5e | ||
|
|
1d9b4d4778 | ||
|
|
baa8a1e6ec | ||
|
|
c116fa645c | ||
|
|
8b4e8199d7 | ||
|
|
34cd5966b9 | ||
|
|
cc8748313e | ||
|
|
1207acc495 | ||
|
|
c29cd83213 | ||
|
|
9df996cb3d | ||
|
|
d2ac7fc90c | ||
|
|
92855ba399 | ||
|
|
b5b7a5d7d0 | ||
|
|
36830266e2 | ||
|
|
3ffbcbf201 | ||
|
|
9997f263a6 | ||
|
|
1fdbd504c7 | ||
|
|
06627cb0bb | ||
|
|
2a0681f0c0 | ||
|
|
67f83d51c9 | ||
|
|
abe7dddfbc | ||
|
|
8b3f9ad8e1 | ||
|
|
f26264ac23 | ||
|
|
cbb13aa4d4 | ||
|
|
86cfad545a | ||
|
|
c786c6a072 | ||
|
|
8153aea64c | ||
|
|
ebc1c73585 | ||
|
|
306efa4d82 | ||
|
|
0fbd980d38 | ||
|
|
0a50c0d27e | ||
|
|
a8405b86de | ||
|
|
6b447951f1 | ||
|
|
788f248de4 | ||
|
|
9336d7f776 | ||
|
|
685822df0d | ||
|
|
c26f605d4c | ||
|
|
0525f43b34 | ||
|
|
f61ff9fd46 | ||
|
|
adcf841a39 | ||
|
|
71195b5370 | ||
|
|
e2b8002d4a | ||
|
|
ea79194925 | ||
|
|
89b320a5e2 | ||
|
|
5e8133b4be | ||
|
|
4cbfd86db0 | ||
|
|
b6885a67e7 | ||
|
|
9f3e3f1700 | ||
|
|
8f6e473aa7 | ||
|
|
683f85f93d | ||
|
|
8959d6ae1d | ||
|
|
17b1eed0d2 | ||
|
|
72eb7e245d | ||
|
|
28108fbe2a | ||
|
|
83551eaf60 | ||
|
|
56a1a7613c | ||
|
|
c580b6bf4c | ||
|
|
54cbbf600d | ||
|
|
d441bd3e53 | ||
|
|
098b2252d0 | ||
|
|
915fa49ce3 | ||
|
|
aa7ff23929 | ||
|
|
1102f5a4d0 | ||
|
|
9aa5a2452a | ||
|
|
edea440cd8 | ||
|
|
28ef0c85be | ||
|
|
d05cfcd6a7 | ||
|
|
a54426ccb8 |
@@ -1,29 +0,0 @@
|
||||
FROM node:14
|
||||
|
||||
# Configure apt
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update \
|
||||
&& apt-get -y install --no-install-recommends apt-utils 2>&1
|
||||
|
||||
# Verify git and process tools are installed
|
||||
RUN apt-get install -y git procps
|
||||
|
||||
# Install yarn, puppeteer deps
|
||||
RUN apt-get install -y curl apt-transport-https lsb-release \
|
||||
&& curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \
|
||||
&& echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \
|
||||
&& apt-get update \
|
||||
&& apt-get -y install --no-install-recommends \
|
||||
yarn fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst ttf-freefont \
|
||||
# https://github.com/Googlechrome/puppeteer/issues/290#issuecomment-322921352
|
||||
gconf-service libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 \
|
||||
libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 \
|
||||
libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
|
||||
libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 \
|
||||
ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||
|
||||
# Clean up
|
||||
RUN apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
ENV DEBIAN_FRONTEND=dialog
|
||||
@@ -1,10 +1,20 @@
|
||||
{
|
||||
"name": "xterm.js",
|
||||
"dockerFile": "Dockerfile",
|
||||
"appPort": 3000,
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
|
||||
"features": {
|
||||
"ghcr.io/devcontainers/features/node:1": {} // yarn
|
||||
},
|
||||
"forwardPorts": [
|
||||
3000
|
||||
],
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"hbenl.vscode-mocha-test-adapter"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-2
@@ -7,6 +7,7 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"demo/tsconfig.json",
|
||||
"src/browser/tsconfig.json",
|
||||
"src/common/tsconfig.json",
|
||||
"src/headless/tsconfig.json",
|
||||
@@ -14,6 +15,7 @@
|
||||
"test/benchmark/tsconfig.json",
|
||||
"addons/xterm-addon-attach/src/tsconfig.json",
|
||||
"addons/xterm-addon-attach/test/tsconfig.json",
|
||||
"addons/xterm-addon-canvas/src/tsconfig.json",
|
||||
"addons/xterm-addon-fit/src/tsconfig.json",
|
||||
"addons/xterm-addon-fit/test/tsconfig.json",
|
||||
"addons/xterm-addon-ligatures/src/tsconfig.json",
|
||||
@@ -37,7 +39,8 @@
|
||||
"**/*.js"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-extra-semi": "error",
|
||||
@@ -94,7 +97,19 @@
|
||||
{ "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"] },
|
||||
// restrict on* naming for events only
|
||||
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
// typeLike
|
||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||
@@ -127,6 +142,9 @@
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"jsdoc/no-multi-asterisks": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"new-parens": "warn",
|
||||
"no-duplicate-imports": "warn",
|
||||
@@ -146,6 +164,29 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"no-restricted-syntax": [
|
||||
"warn",
|
||||
{
|
||||
"selector": "CallExpression[callee.name='requestAnimationFrame']",
|
||||
"message": "The global requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression[callee.name='cancelAnimationFrame']",
|
||||
"message": "The global cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='requestAnimationFrame']",
|
||||
"message": "window.requestAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "CallExpression > MemberExpression[object.name='window'][property.name='cancelAnimationFrame']",
|
||||
"message": "window.cancelAnimationFrame() should be avoided, call it on the parent window from ICoreBrowserService."
|
||||
},
|
||||
{
|
||||
"selector": "MemberExpression[object.name='window'][property.name='devicePixelRatio']",
|
||||
"message": "window.devicePixelRatio should be avoided, get it from ICoreBrowserService."
|
||||
}
|
||||
],
|
||||
"no-trailing-spaces": "warn",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-var": "warn",
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://stackoverflow.com/questions/tagged/xtermjs
|
||||
about: Please ask and answer questions here.
|
||||
- name: FAQ
|
||||
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
|
||||
about: See our frequently asked questions before filing a bug report
|
||||
- name: Support / Q&A
|
||||
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
|
||||
about: Use GitHub Discussions for community support and general Q&A
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master" ]
|
||||
schedule:
|
||||
- cron: '41 17 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -1,11 +1,5 @@
|
||||
require:
|
||||
- source-map-support/register
|
||||
spec:
|
||||
- out/**/*.test.js
|
||||
- addons/**/out/*.test.js
|
||||
watch-files:
|
||||
- out/**/*.js
|
||||
- addons/**/out/*.js
|
||||
reporter: spec
|
||||
color: true
|
||||
check-leaks: true
|
||||
|
||||
Vendored
+11
-3
@@ -1,5 +1,13 @@
|
||||
{
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"mochaExplorer.envPath": ".mocha.env"
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"mochaExplorer.envPath": ".mocha.env",
|
||||
"mochaExplorer.files": [
|
||||
"out/**/*.test.js",
|
||||
"addons/**/out/*.test.js"
|
||||
],
|
||||
"mochaExplorer.watch": [
|
||||
"out/**/*.js",
|
||||
"addons/**/out/*.js"
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+1
-2
@@ -32,7 +32,6 @@
|
||||
"label": "start",
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"dependsOn": "watch",
|
||||
"group": "build",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [],
|
||||
@@ -42,7 +41,7 @@
|
||||
},
|
||||
{
|
||||
"label": "Start demo",
|
||||
"dependsOn": "start",
|
||||
"dependsOn": ["start", "watch"],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
|
||||
@@ -87,8 +87,6 @@ The xterm.js team maintains the following addons, but anyone can build them:
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*.
|
||||
|
||||
We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working.
|
||||
|
||||
Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working.
|
||||
|
||||
### Node.js Support
|
||||
@@ -101,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file](
|
||||
|
||||
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.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
|
||||
```bash
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
@@ -142,7 +160,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
|
||||
- [**Hyper**](https://hyper.is): A terminal built on web technologies.
|
||||
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
|
||||
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
|
||||
- [**GoTTY**](https://github.com/sorenisanerd/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
|
||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js.
|
||||
@@ -168,7 +186,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**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.
|
||||
- [**Replit**](https://replit.com): 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.
|
||||
@@ -192,34 +210,20 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**hack.courses**](https://hack.courses): Interactive Linux and command-line classes using xterm.js to expose a real terminal available for everyone.
|
||||
- [**Render**](https://render.com): Platform-as-a-service for your apps, websites, and databases using xterm.js to provide a command prompt for user containers and for streaming build and runtime logs.
|
||||
- [**CloudTTY**](https://github.com/cloudtty/cloudtty): A Friendly Kubernetes CloudShell (Web Terminal).
|
||||
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
|
||||
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
|
||||
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
|
||||
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
|
||||
- [**graSSHopper**](https://grasshopper.coding.kiwi): A simple SSH client with file explorer, history and many more features.
|
||||
- [**DomTerm**](https://domterm.org/xtermjs.html): Tiles and tabs. Detachable sessions (like tmux). [Remote connections](https://domterm.org/Remoting-over-ssh.html) using a nice ssh wrapper with predictive echo. Qt, Electron, Tauri/Wry, or desktop browser front-ends. Choose between xterm.js engine (faster) or native DomTerm (more functionality and graphics) - or both.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
|
||||
```bash
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
|
||||
## License Agreement
|
||||
|
||||
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-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-attach",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -21,6 +21,6 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
// TODO: do something better than just swallowing
|
||||
// the data if the socket is not in a working condition
|
||||
if (this._socket.readyState !== 1) {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
private _sendBinary(data: string): void {
|
||||
if (this._socket.readyState !== 1) {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
const buffer = new Uint8Array(data.length);
|
||||
@@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
this._socket.send(buffer);
|
||||
}
|
||||
|
||||
private _checkOpenSocket(): boolean {
|
||||
switch (this._socket.readyState) {
|
||||
case WebSocket.OPEN:
|
||||
return true;
|
||||
case WebSocket.CONNECTING:
|
||||
throw new Error('Attach addon was loaded before socket was open');
|
||||
case WebSocket.CLOSING:
|
||||
console.warn('Attach addon socket is closing');
|
||||
return false;
|
||||
case WebSocket.CLOSED:
|
||||
throw new Error('Attach addon socket is closed');
|
||||
default:
|
||||
throw new Error('Unexpected socket state');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
|
||||
@@ -28,7 +28,7 @@ describe('AttachAddon', () => {
|
||||
beforeEach(async () => await page.goto(APP));
|
||||
|
||||
it('string', async function(): Promise<any> {
|
||||
await openTerminal(page, { rendererType: 'dom' });
|
||||
await openTerminal(page);
|
||||
const port = 8080;
|
||||
const server = new WebSocket.Server({ port });
|
||||
server.on('connection', socket => socket.send('foo'));
|
||||
@@ -38,7 +38,7 @@ describe('AttachAddon', () => {
|
||||
});
|
||||
|
||||
it('utf8', async function(): Promise<any> {
|
||||
await openTerminal(page, { rendererType: 'dom' });
|
||||
await openTerminal(page);
|
||||
const port = 8080;
|
||||
const server = new WebSocket.Server({ port });
|
||||
const data = new Uint8Array([102, 111, 111]);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm';
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-attach' {
|
||||
export interface IAttachOptions {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
lib
|
||||
node_modules
|
||||
@@ -0,0 +1,29 @@
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Blacklist - src/ test files
|
||||
src/**/*.test.ts
|
||||
src/**/*.test.d.ts
|
||||
src/**/*.test.js
|
||||
src/**/*.test.js.map
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2017, 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,28 @@
|
||||
## xterm-addon-canvas
|
||||
|
||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a canvas-based renderer using a 2d context to draw. This addon requires xterm.js v5+.
|
||||
|
||||
The purpose of this addon is to be used as a fallback for the [webgl addon](https://www.npmjs.com/package/xterm-addon-webgl) when better performance is desired over the default DOM renderer, but WebGL2 isn't supported or performant for some reason.
|
||||
|
||||
### Install
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-canvas
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { CanvasAddon } from 'xterm-addon-canvas';
|
||||
|
||||
const terminal = new Terminal();
|
||||
terminal.open(element);
|
||||
terminal.loadAddon(new CanvasAddon());
|
||||
```
|
||||
|
||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts) for more advanced usage.
|
||||
|
||||
### See also
|
||||
|
||||
- [xterm-addon-webgl](https://www.npmjs.com/package/xterm-addon-webgl) A renderer for xterm.js that uses WebGL
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "xterm-addon-canvas",
|
||||
"version": "0.4.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-canvas.js",
|
||||
"types": "typings/xterm-addon-canvas.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"canvas",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/tsc -p .",
|
||||
"prepackage": "npm run build",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICellData, IDisposable } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IRenderLayer } from './Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
|
||||
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _deviceCharWidth: number = 0;
|
||||
private _deviceCharHeight: number = 0;
|
||||
private _deviceCellWidth: number = 0;
|
||||
private _deviceCellHeight: number = 0;
|
||||
private _deviceCharLeft: number = 0;
|
||||
private _deviceCharTop: number = 0;
|
||||
|
||||
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
|
||||
|
||||
protected _charAtlas!: ITextureAtlas;
|
||||
private _charAtlasDisposable?: IDisposable;
|
||||
|
||||
public get canvas(): HTMLCanvasElement { return this._canvas; }
|
||||
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
|
||||
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected readonly _themeService: IThemeService,
|
||||
protected readonly _bufferService: IBufferService,
|
||||
protected readonly _optionsService: IOptionsService,
|
||||
protected readonly _decorationService: IDecorationService,
|
||||
protected readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super();
|
||||
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._refreshCharAtlas(e);
|
||||
this.reset();
|
||||
// Trigger selection changed as it's handled separately to regular rendering
|
||||
this.handleSelectionChanged(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}));
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
this._canvas.remove();
|
||||
this._charAtlas?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = throwIfFalsy(this._canvas.getContext('2d', { alpha: this._alpha }));
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public handleBlur(): void {}
|
||||
public handleFocus(): void {}
|
||||
public handleCursorMove(): void {}
|
||||
public handleGridChanged(startRow: number, endRow: number): void {}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._selectionModel.update(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
protected _setTransparency(alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
if (alpha === this._alpha) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new canvas and replace old one
|
||||
const oldCanvas = this._canvas;
|
||||
this._alpha = alpha;
|
||||
// Cloning preserves properties
|
||||
this._canvas = this._canvas.cloneNode() as HTMLCanvasElement;
|
||||
this._initCanvas();
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
this.handleGridChanged(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
|
||||
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
this._charAtlas.warmUp();
|
||||
for (let i = 0; i < this._charAtlas.pages.length; i++) {
|
||||
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
this._deviceCellWidth = dim.device.cell.width;
|
||||
this._deviceCellHeight = dim.device.cell.height;
|
||||
this._deviceCharWidth = dim.device.char.width;
|
||||
this._deviceCharHeight = dim.device.char.height;
|
||||
this._deviceCharLeft = dim.device.char.left;
|
||||
this._deviceCharTop = dim.device.char.top;
|
||||
this._canvas.width = dim.device.canvas.width;
|
||||
this._canvas.height = dim.device.canvas.height;
|
||||
this._canvas.style.width = `${dim.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${dim.css.canvas.height}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
}
|
||||
|
||||
public abstract reset(): void;
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._charAtlas?.clearTexture();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills 1+ cells completely. This uses the existing fillStyle on the context.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at
|
||||
* @param width The number of columns to fill.
|
||||
* @param height The number of rows to fill.
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
const cellOffset = Math.ceil(this._deviceCellHeight * 0.5);
|
||||
this._ctx.fillRect(
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the bottom of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
protected _curlyUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
const xLeft = (x + xOffset) * this._deviceCellWidth;
|
||||
const xMid = (x + xOffset + 0.5) * this._deviceCellWidth;
|
||||
const xRight = (x + xOffset + 1) * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
const yMidBot = yMid - lineWidth;
|
||||
const yMidTop = yMid + lineWidth;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
this._ctx.bezierCurveTo(
|
||||
xLeft, yMidBot,
|
||||
xMid, yMidBot,
|
||||
xMid, yMid
|
||||
);
|
||||
this._ctx.bezierCurveTo(
|
||||
xMid, yMidTop,
|
||||
xRight, yMidTop,
|
||||
xRight, yMid
|
||||
);
|
||||
}
|
||||
this._ctx.stroke();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
protected _dottedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
|
||||
const xLeft = x * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
// const xLeft = x * this._deviceCellWidth;
|
||||
const xRight = (x + width + xOffset) * this._deviceCellWidth;
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
}
|
||||
this._ctx.stroke();
|
||||
this._ctx.closePath();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
protected _dashedUnderlineAtCell(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.save();
|
||||
this._ctx.beginPath();
|
||||
this._ctx.strokeStyle = this._ctx.fillStyle;
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
|
||||
const xLeft = x * this._deviceCellWidth;
|
||||
const xRight = (x + width) * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
this._ctx.stroke();
|
||||
this._ctx.closePath();
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the left of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
this._coreBrowserService.dpr * width,
|
||||
this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strokes a 1px rectangle (2px on HDPI) around a cell. This uses the existing
|
||||
* strokeStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.strokeRect(
|
||||
x * this._deviceCellWidth + lineWidth / 2,
|
||||
y * this._deviceCellHeight + (lineWidth / 2),
|
||||
width * this._deviceCellWidth - lineWidth,
|
||||
(height * this._deviceCellHeight) - lineWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the entire canvas.
|
||||
*/
|
||||
protected _clearAll(): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears 1+ cells completely.
|
||||
* @param x The column to start at.
|
||||
* @param y The row to start at.
|
||||
* @param width The number of columns to clear.
|
||||
* @param height The number of rows to clear.
|
||||
*/
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a truecolor character at the cell. The character will be clipped to
|
||||
* ensure that it fits with the cell, including the cell to the right if it's
|
||||
* a wide character. This uses the existing fillStyle on the context.
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
this._clipRow(y);
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.rawOptions.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._deviceCellWidth + this._deviceCharLeft,
|
||||
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
*/
|
||||
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
||||
const chars = cell.getChars();
|
||||
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
|
||||
let glyph: IRasterizedGlyph;
|
||||
if (chars && chars.length > 1) {
|
||||
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||
} else {
|
||||
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||
}
|
||||
if (!glyph.size.x || !glyph.size.y) {
|
||||
return;
|
||||
}
|
||||
this._ctx.save();
|
||||
this._clipRow(y);
|
||||
|
||||
// Draw the image, use the bitmap if it's available
|
||||
|
||||
// HACK: If the canvas doesn't match, delete the generator. It's not clear how this happens but
|
||||
// something is wrong with either the lifecycle of _bitmapGenerator or the page canvases are
|
||||
// swapped out unexpectedly
|
||||
if (this._bitmapGenerator[glyph.texturePage] && this._charAtlas.pages[glyph.texturePage].canvas !== this._bitmapGenerator[glyph.texturePage]!.canvas) {
|
||||
this._bitmapGenerator[glyph.texturePage]?.bitmap?.close();
|
||||
delete this._bitmapGenerator[glyph.texturePage];
|
||||
}
|
||||
|
||||
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
|
||||
if (!this._bitmapGenerator[glyph.texturePage]) {
|
||||
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
|
||||
}
|
||||
this._bitmapGenerator[glyph.texturePage]!.refresh();
|
||||
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
|
||||
}
|
||||
this._ctx.drawImage(
|
||||
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
|
||||
glyph.texturePosition.x,
|
||||
glyph.texturePosition.y,
|
||||
glyph.size.x,
|
||||
glyph.size.y,
|
||||
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
|
||||
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
|
||||
glyph.size.x,
|
||||
glyph.size.y
|
||||
);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param y The row to clip.
|
||||
*/
|
||||
private _clipRow(y: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._deviceCellHeight,
|
||||
this._bufferService.cols * this._deviceCellWidth,
|
||||
this._deviceCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the current font.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? this._optionsService.rawOptions.fontWeightBold : this._optionsService.rawOptions.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
|
||||
* the operation as window.createImageBitmap is asynchronous.
|
||||
*/
|
||||
const GLYPH_BITMAP_COMMIT_DELAY = 100;
|
||||
|
||||
const enum BitmapGeneratorState {
|
||||
IDLE = 0,
|
||||
GENERATING = 1,
|
||||
GENERATING_INVALID = 2
|
||||
}
|
||||
|
||||
class BitmapGenerator {
|
||||
private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
|
||||
private _commitTimeout: number | undefined = undefined;
|
||||
private _bitmap: ImageBitmap | undefined = undefined;
|
||||
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
|
||||
public version: number = -1;
|
||||
|
||||
constructor(public readonly canvas: HTMLCanvasElement) {
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// Clear the bitmap immediately as it's stale
|
||||
this._bitmap?.close();
|
||||
this._bitmap = undefined;
|
||||
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
|
||||
if (isSafari) {
|
||||
return;
|
||||
}
|
||||
if (this._commitTimeout === undefined) {
|
||||
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
|
||||
}
|
||||
if (this._state === BitmapGeneratorState.GENERATING) {
|
||||
this._state = BitmapGeneratorState.GENERATING_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
private _generate(): void {
|
||||
if (this._state === BitmapGeneratorState.IDLE) {
|
||||
this._bitmap?.close();
|
||||
this._bitmap = undefined;
|
||||
this._state = BitmapGeneratorState.GENERATING;
|
||||
window.createImageBitmap(this.canvas).then(bitmap => {
|
||||
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
|
||||
this.refresh();
|
||||
} else {
|
||||
this._bitmap = bitmap;
|
||||
}
|
||||
this._state = BitmapGeneratorState.IDLE;
|
||||
});
|
||||
if (this._commitTimeout) {
|
||||
this._commitTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { CanvasRenderer } from './CanvasRenderer';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export class CanvasAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: CanvasRenderer;
|
||||
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._renderer?.textureAtlas;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
const core = (terminal as any)._core as ITerminal;
|
||||
if (!terminal.element) {
|
||||
this.register(core.onWillOpen(() => this.activate(terminal)));
|
||||
return;
|
||||
}
|
||||
|
||||
this._terminal = terminal;
|
||||
const coreService = core.coreService;
|
||||
const optionsService = core.optionsService;
|
||||
const screenElement = core.screenElement!;
|
||||
const linkifier = core.linkifier2;
|
||||
|
||||
const unsafeCore = core as any;
|
||||
const bufferService: IBufferService = unsafeCore._bufferService;
|
||||
const renderService: IRenderService = unsafeCore._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
renderService.handleResize(bufferService.cols, bufferService.rows);
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.handleResize(terminal.cols, terminal.rows);
|
||||
this._renderer?.dispose();
|
||||
this._renderer = undefined;
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2 } from 'browser/Types';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { IRenderLayer } from './Types';
|
||||
|
||||
export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
linkifier2: ILinkifier2,
|
||||
private readonly _bufferService: IBufferService,
|
||||
private readonly _charSizeService: ICharSizeService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
characterJoinerService: ICharacterJoinerService,
|
||||
coreService: ICoreService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService,
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService),
|
||||
new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService),
|
||||
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
|
||||
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
|
||||
];
|
||||
for (const layer of this._renderLayers) {
|
||||
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
}
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
this.register(toDisposable(() => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
removeTerminalFromCache(this._terminal);
|
||||
}));
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._renderLayers[0].cacheCanvas;
|
||||
}
|
||||
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this.handleResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
// Resize all render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this.dimensions);
|
||||
}
|
||||
|
||||
// Resize the screen
|
||||
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
}
|
||||
|
||||
public handleCharSizeChanged(): void {
|
||||
this.handleResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
|
||||
public handleBlur(): void {
|
||||
this._runOperation(l => l.handleBlur());
|
||||
}
|
||||
|
||||
public handleFocus(): void {
|
||||
this._runOperation(l => l.handleFocus());
|
||||
}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode));
|
||||
// Selection foreground requires a full re-render
|
||||
if (this._themeService.colors.selectionForeground) {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
public handleCursorMove(): void {
|
||||
this._runOperation(l => l.handleCursorMove());
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._runOperation(l => l.reset());
|
||||
}
|
||||
|
||||
private _runOperation(operation: (layer: IRenderLayer) => void): void {
|
||||
for (const l of this._renderLayers) {
|
||||
operation(l);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the refresh loop callback, calling refresh only if a refresh is
|
||||
* necessary before queueing up the next one.
|
||||
*/
|
||||
public renderRows(start: number, end: number): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.handleGridChanged(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
for (const layer of this._renderLayers) {
|
||||
layer.clearTextureAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
if (!this._charSizeService.hasValidSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
// See the WebGL renderer for an explanation of this section.
|
||||
const dpr = this._coreBrowserService.dpr;
|
||||
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr);
|
||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
|
||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width;
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
|
||||
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
|
||||
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
this.dimensions.device.canvas.height = height;
|
||||
this.dimensions.device.canvas.width = width;
|
||||
// Resize all render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this.dimensions);
|
||||
}
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
+51
-53
@@ -3,14 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { isFirefox } from 'common/Platform';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -32,18 +35,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
private _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _coreService: ICoreService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -56,14 +59,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this.register(toDisposable(() => {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}
|
||||
super.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -81,25 +81,25 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
public reset(): void {
|
||||
this._clearCursor();
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
this.onOptionsChanged();
|
||||
this._handleOptionsChanged();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
public handleFocus(): void {
|
||||
this._cursorBlinkStateManager?.resume();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
private _handleOptionsChanged(): void {
|
||||
if (this._optionsService.rawOptions.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
|
||||
this._render(true);
|
||||
});
|
||||
}, this._coreBrowserService);
|
||||
}
|
||||
} else {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
@@ -110,11 +110,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
public handleCursorMove(): void {
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
}
|
||||
|
||||
public onGridChanged(startRow: number, endRow: number): void {
|
||||
public handleGridChanged(startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(false);
|
||||
} else {
|
||||
@@ -148,13 +148,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
|
||||
}
|
||||
this._renderBlurCursor(cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
@@ -195,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
// Avoid potential rounding errors when device pixel ratio is less than 1
|
||||
if (window.devicePixelRatio < 1) {
|
||||
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
|
||||
// less than 1
|
||||
if (isFirefox || this._coreBrowserService.dpr < 1) {
|
||||
this._clearAll();
|
||||
} else {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
@@ -213,30 +210,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
@@ -258,7 +255,8 @@ class CursorBlinkStateManager {
|
||||
|
||||
constructor(
|
||||
isFocused: boolean,
|
||||
private _renderCallback: () => void
|
||||
private _renderCallback: () => void,
|
||||
private _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
this.isCursorVisible = true;
|
||||
if (isFocused) {
|
||||
@@ -270,15 +268,15 @@ class CursorBlinkStateManager {
|
||||
|
||||
public dispose(): void {
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
@@ -292,7 +290,7 @@ class CursorBlinkStateManager {
|
||||
// Force a cursor render to ensure it's visible and in the correct position
|
||||
this.isCursorVisible = true;
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
@@ -302,7 +300,7 @@ class CursorBlinkStateManager {
|
||||
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
|
||||
// Clear any existing interval
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
|
||||
@@ -310,7 +308,7 @@ class CursorBlinkStateManager {
|
||||
// the regular interval is setup in order to support restarting the blink
|
||||
// animation in a lightweight way (without thrashing clearInterval and
|
||||
// setInterval).
|
||||
this._blinkStartTimeout = window.setTimeout(() => {
|
||||
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
|
||||
// Check if another animation restart was requested while this was being
|
||||
// started
|
||||
if (this._animationTimeRestarted) {
|
||||
@@ -324,13 +322,13 @@ class CursorBlinkStateManager {
|
||||
|
||||
// Hide the cursor
|
||||
this.isCursorVisible = false;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
|
||||
// Setup the blink interval
|
||||
this._blinkInterval = window.setInterval(() => {
|
||||
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
|
||||
// Adjust the animation time if it was restarted
|
||||
if (this._animationTimeRestarted) {
|
||||
// calc time diff
|
||||
@@ -343,7 +341,7 @@ class CursorBlinkStateManager {
|
||||
|
||||
// Invert visibility and render
|
||||
this.isCursorVisible = !this.isCursorVisible;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
@@ -354,15 +352,15 @@ class CursorBlinkStateManager {
|
||||
public pause(): void {
|
||||
this.isCursorVisible = true;
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { GridCache } from './GridCache';
|
||||
|
||||
describe('GridCache', () => {
|
||||
let grid: GridCache<number>;
|
||||
+20
-20
@@ -3,33 +3,33 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
linkifier: ILinkifier,
|
||||
linkifier2: ILinkifier2,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
decorationService: IDecorationService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
|
||||
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
|
||||
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -54,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
} else if (e.fg && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
@@ -78,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
+35
-14
@@ -3,10 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
interface ISelectionState {
|
||||
start?: [number, number];
|
||||
@@ -19,15 +20,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state!: ISelectionState;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
bufferService: IBufferService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService,
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
@@ -42,8 +44,12 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._clearState();
|
||||
// On resize use the base render layer's cached selection values since resize clears _state
|
||||
// inside reset.
|
||||
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
|
||||
this._clearState();
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
@@ -53,9 +59,22 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.onSelectionChanged(start, end, columnSelectMode);
|
||||
public handleBlur(): void {
|
||||
this.reset();
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
|
||||
public handleFocus(): void {
|
||||
this.reset();
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.handleSelectionChanged(start, end, columnSelectMode);
|
||||
this._redrawSelection(start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
private _redrawSelection(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
// Selection has not changed
|
||||
if (!this._didStateChange(start, end, columnSelectMode, this._bufferService.buffer.ydisp)) {
|
||||
return;
|
||||
@@ -82,7 +101,9 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
this._ctx.fillStyle = this._colors.selectionTransparent.css;
|
||||
this._ctx.fillStyle = (this._coreBrowserService.isFocused
|
||||
? this._themeService.colors.selectionBackgroundTransparent
|
||||
: this._themeService.colors.selectionInactiveBackgroundTransparent).css;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = start[0];
|
||||
+34
-84
@@ -3,17 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { GridCache } from './GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { color, css } from 'common/Color';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -30,18 +32,20 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
alpha: boolean,
|
||||
rendererId: number,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@IDecorationService decorationService: IDecorationService
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
decorationService: IDecorationService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService);
|
||||
super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._state = new GridCache<CharData>();
|
||||
this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -49,8 +53,8 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// Clear the character width cache if the font or width has changed
|
||||
const terminalFont = this._getFont(false, false);
|
||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.device.char.width;
|
||||
this._characterFont = terminalFont;
|
||||
this._characterOverlapCache = {};
|
||||
}
|
||||
@@ -91,6 +95,13 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// exit early for NULL and SP
|
||||
// NOTE: commented out due to #4120 (needs a more clever patch to keep things performant)
|
||||
// const code = cell.getCode();
|
||||
// if (code === 0 || code === 32) {
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// 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.
|
||||
@@ -165,30 +176,30 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isFgDefault()) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
nextFillStyle = this._themeService.colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
|
||||
nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
|
||||
}
|
||||
} else if (cell.isBgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else if (cell.isBgPalette()) {
|
||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let isTop = false;
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, this._bufferService.buffer.ydisp + y)) {
|
||||
this._decorationService.forEachDecorationAtCell(x, this._bufferService.buffer.ydisp + y, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
nextFillStyle = d.backgroundColorRGB.css;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
}
|
||||
});
|
||||
|
||||
if (prevFillStyle === null) {
|
||||
// This is either the first iteration, or the default background was set. Either way, we
|
||||
@@ -224,52 +235,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
return;
|
||||
}
|
||||
this._drawChars(cell, x, y);
|
||||
if (cell.isUnderline() || cell.isStrikethrough()) {
|
||||
this._ctx.save();
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
} else {
|
||||
if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isStrikethrough()) {
|
||||
this._fillMiddleLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
this._ctx.restore();
|
||||
}
|
||||
});
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
|
||||
}
|
||||
|
||||
public onGridChanged(firstRow: number, lastRow: number): void {
|
||||
public handleGridChanged(firstRow: number, lastRow: number): void {
|
||||
// Resize has not been called yet
|
||||
if (this._state.cache.length === 0) {
|
||||
return;
|
||||
@@ -284,10 +253,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._drawForeground(firstRow, lastRow);
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a character is overlapping to the next cell.
|
||||
*/
|
||||
@@ -326,19 +291,4 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._characterOverlapCache[chars] = overlaps;
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the charcater at the cell specified.
|
||||
* @param x The column of the char.
|
||||
* @param y The row of the char.
|
||||
*/
|
||||
// private _clearChar(x: number, y: number): void {
|
||||
// let colsToClear = 1;
|
||||
// // Clear the adjacent character if it was wide
|
||||
// const state = this._state.cache[x][y];
|
||||
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// colsToClear = 2;
|
||||
// }
|
||||
// this.clearCells(x, y, colsToClear, 1);
|
||||
// }
|
||||
}
|
||||
+19
-42
@@ -4,23 +4,8 @@
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
export interface IRenderDimensions {
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharLeft: number;
|
||||
scaledCharTop: number;
|
||||
scaledCanvasWidth: number;
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
actualCellWidth: number;
|
||||
actualCellHeight: number;
|
||||
}
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
@@ -40,57 +25,49 @@ export interface IRenderer extends IDisposable {
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
handleDevicePixelRatioChange(): void;
|
||||
handleResize(cols: number, rows: number): void;
|
||||
handleCharSizeChanged(): void;
|
||||
handleBlur(): void;
|
||||
handleFocus(): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleCursorMove(): void;
|
||||
handleOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
}
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly cacheCanvas: HTMLCanvasElement;
|
||||
|
||||
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(): void;
|
||||
handleBlur(): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
* Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(): void;
|
||||
handleFocus(): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(colorSet: IColorSet): void;
|
||||
handleCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(startRow: number, endRow: number): void;
|
||||
handleGridChanged(startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"common/*": [
|
||||
"../../../src/common/*"
|
||||
],
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
},
|
||||
"strict": true,
|
||||
"downlevelIteration": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/common"
|
||||
},
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-canvas' {
|
||||
/**
|
||||
* An xterm.js addon that provides search functionality.
|
||||
*/
|
||||
export class CanvasAddon implements ITerminalAddon {
|
||||
public textureAtlas?: HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* An event that is fired when the texture atlas of the renderer changes.
|
||||
*/
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Clears the terminal's texture atlas and triggers a redraw.
|
||||
*/
|
||||
public clearTextureAtlas(): void;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'CanvasAddon';
|
||||
const mainFile = 'xterm-addon-canvas.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'),
|
||||
browser: path.resolve('../../out/browser')
|
||||
}
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-fit",
|
||||
"version": "0.5.0",
|
||||
"version": "0.7.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -21,6 +21,6 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
/**
|
||||
@@ -58,8 +59,9 @@ export class FitAddon implements ITerminalAddon {
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (this._terminal as any)._core;
|
||||
const dims: IRenderDimensions = core._renderService.dimensions;
|
||||
|
||||
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
|
||||
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -81,8 +83,8 @@ export class FitAddon implements ITerminalAddon {
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
@@ -13,10 +13,20 @@
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ describe('FitAddon', () => {
|
||||
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.cols, 85);
|
||||
assert.isBelow(dimensions.cols, 88);
|
||||
assert.isAbove(dimensions.rows, 24);
|
||||
assert.isBelow(dimensions.rows, 29);
|
||||
});
|
||||
@@ -56,7 +57,8 @@ describe('FitAddon', () => {
|
||||
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.cols, 108);
|
||||
assert.isBelow(dimensions.cols, 111);
|
||||
assert.isAbove(dimensions.rows, 24);
|
||||
assert.isBelow(dimensions.rows, 29);
|
||||
});
|
||||
@@ -89,7 +91,8 @@ describe('FitAddon', () => {
|
||||
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(cols, 85);
|
||||
assert.isBelow(cols, 88);
|
||||
assert.isAbove(rows, 24);
|
||||
assert.isBelow(rows, 29);
|
||||
});
|
||||
@@ -99,7 +102,8 @@ describe('FitAddon', () => {
|
||||
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(cols, 108);
|
||||
assert.isBelow(cols, 111);
|
||||
assert.isAbove(rows, 24);
|
||||
assert.isBelow(rows, 29);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ declare module 'xterm-addon-fit' {
|
||||
/**
|
||||
* Gets the proposed dimensions that will be used for a fit.
|
||||
*/
|
||||
public proposeDimensions(): ITerminalDimensions;
|
||||
public proposeDimensions(): ITerminalDimensions | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -33,15 +33,16 @@ This package locates the font file on disk for the font currently in use by the
|
||||
|
||||
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]).
|
||||
|
||||
### Fallback Ligatures
|
||||
|
||||
When ligatures cannot be fetched from the environment, a set of "fallback" ligatures is used to get the most common ligatures working. These fallback ligatures can be customized with options passed to `LigatureAddon.constructor`.
|
||||
|
||||
### 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
|
||||
* [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/
|
||||
|
||||
@@ -22,10 +22,13 @@ const fontsFolder = path.join(__dirname, '../fonts');
|
||||
async function download() {
|
||||
await mkdirp(fontsFolder);
|
||||
|
||||
await downloadFiraCode();
|
||||
await downloadIosevka();
|
||||
|
||||
console.log('Loaded all fonts for testing')
|
||||
try {
|
||||
await downloadFiraCode();
|
||||
await downloadIosevka();
|
||||
console.log('Loaded all fonts for testing')
|
||||
} catch (e) {
|
||||
console.warn('Fonts failed to download, ligature tests will not work', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFiraCode() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-ligatures",
|
||||
"version": "0.5.3",
|
||||
"version": "0.6.0",
|
||||
"description": "Add support for programming ligatures to xterm.js",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
@@ -42,6 +42,6 @@
|
||||
"yauzl": "^2.10.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { enableLigatures } from '.';
|
||||
import { ILigatureOptions } from './Types';
|
||||
|
||||
export interface ITerminalAddon {
|
||||
activate(terminal: Terminal): void;
|
||||
@@ -12,12 +13,31 @@ export interface ITerminalAddon {
|
||||
}
|
||||
|
||||
export class LigaturesAddon implements ITerminalAddon {
|
||||
constructor() {}
|
||||
private readonly _fallbackLigatures: string[];
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
enableLigatures(terminal);
|
||||
private _terminal: Terminal | undefined;
|
||||
private _characterJoinerId: number | undefined;
|
||||
|
||||
constructor(options?: Partial<ILigatureOptions>) {
|
||||
this._fallbackLigatures = (options?.fallbackLigatures || [
|
||||
'<--', '<---', '<<-', '<-', '->', '->>', '-->', '--->',
|
||||
'<==', '<===', '<<=', '<=', '=>', '=>>', '==>', '===>', '>=', '>>=',
|
||||
'<->', '<-->', '<--->', '<---->', '<=>', '<==>', '<===>', '<====>', '-------->',
|
||||
'<~~', '<~', '~>', '~~>', '::', ':::', '==', '!=', '===', '!==',
|
||||
':=', ':-', ':+', '<*', '<*>', '*>', '<|', '<|>', '|>', '+:', '-:', '=:', ':>',
|
||||
'++', '+++', '<!--', '<!---', '<***>'
|
||||
]).sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
public dispose(): void {}
|
||||
}
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._characterJoinerId = enableLigatures(terminal, this._fallbackLigatures);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._characterJoinerId !== undefined) {
|
||||
this._terminal?.deregisterCharacterJoiner(this._characterJoinerId);
|
||||
this._characterJoinerId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface ILigatureOptions {
|
||||
fallbackLigatures: string[];
|
||||
}
|
||||
@@ -3,8 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontList } from 'font-finder';
|
||||
import { Font, loadBuffer, loadFile } from 'font-ligatures';
|
||||
import { Font, loadBuffer } from 'font-ligatures';
|
||||
|
||||
import parse from './parse';
|
||||
|
||||
@@ -24,7 +23,7 @@ interface IFontAccessNavigator {
|
||||
};
|
||||
}
|
||||
|
||||
let fontsPromise: Promise<FontList | Record<string, IFontMetadata[]>> | undefined = undefined;
|
||||
let fontsPromise: Promise<Record<string, IFontMetadata[]>> | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Loads the font ligature wrapper for the specified font family if it could be
|
||||
@@ -65,12 +64,20 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
// Node environment or no font access API
|
||||
else {
|
||||
// Latest proposal https://bugs.chromium.org/p/chromium/issues/detail?id=1312603
|
||||
else if (typeof process !== 'object' && 'queryLocalFonts' in window) {
|
||||
const fonts: Record<string, IFontMetadata[]> = {};
|
||||
try {
|
||||
fontsPromise = (await import('font-finder')).list();
|
||||
} catch (err) {
|
||||
// No-op
|
||||
const fontsIterator = await (window as any).queryLocalFonts();
|
||||
for (const metadata of fontsIterator) {
|
||||
if (!fonts.hasOwnProperty(metadata.family)) {
|
||||
fonts[metadata.family] = [];
|
||||
}
|
||||
fonts[metadata.family].push(metadata);
|
||||
}
|
||||
fontsPromise = Promise.resolve(fonts);
|
||||
} catch (err: any) {
|
||||
console.error(err.name, err.message);
|
||||
}
|
||||
}
|
||||
if (!fontsPromise) {
|
||||
@@ -90,9 +97,11 @@ export default async function load(fontFamily: string, cacheSize: number): Promi
|
||||
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 });
|
||||
const bytes = await font.blob();
|
||||
const buffer = await bytes.arrayBuffer();
|
||||
return loadBuffer(buffer, { cacheSize });
|
||||
}
|
||||
return await loadFile(font.path, { cacheSize });
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,82 +69,11 @@ describe('xterm-addon-ligatures', () => {
|
||||
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.options.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.options.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.options.fontFamily = 'Fira Code';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.options.fontFamily = 'notinstalled';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
term.options.fontFamily = 'Iosevka';
|
||||
assert.deepEqual(term.joiner!(input), []);
|
||||
onRefresh.callsFake(() => {
|
||||
assert.deepEqual(term.joiner!(input), [[2, 4]]);
|
||||
|
||||
// And switch it back to Fira Code for good measure
|
||||
term.options.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.options.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.options.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 () => {
|
||||
@@ -176,7 +105,6 @@ describe('xterm-addon-ligatures', () => {
|
||||
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 () => {
|
||||
@@ -185,7 +113,6 @@ describe('xterm-addon-ligatures', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import type { Terminal } from 'xterm';
|
||||
import { Font } from 'font-ligatures';
|
||||
|
||||
import load from './font';
|
||||
@@ -26,13 +26,13 @@ const CACHE_SIZE = 100000;
|
||||
* start to render them.
|
||||
* @param term Terminal instance from xterm.js
|
||||
*/
|
||||
export function enableLigatures(term: Terminal): void {
|
||||
export function enableLigatures(term: Terminal, fallbackLigatures: string[] = []): number {
|
||||
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][] => {
|
||||
return term.registerCharacterJoiner((text: string): [number, number][] => {
|
||||
// If the font hasn't been loaded yet, load it and return an empty result
|
||||
const termFont = term.options.fontFamily;
|
||||
if (
|
||||
@@ -54,7 +54,7 @@ export function enableLigatures(term: Terminal): void {
|
||||
|
||||
// Only refresh things if we actually found a font
|
||||
if (f) {
|
||||
term.refresh(0, term.options.rows! - 1);
|
||||
term.refresh(0, term.rows - 1);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -63,6 +63,9 @@ export function enableLigatures(term: Terminal): void {
|
||||
// sure our font is still vaild.
|
||||
if (currentCallFontName === term.options.fontFamily) {
|
||||
loadingState = LoadingState.FAILED;
|
||||
if (term.options.logLevel === 'debug') {
|
||||
console.debug(loadError, new Error('Failure while loading font'));
|
||||
}
|
||||
font = undefined;
|
||||
loadError = e;
|
||||
}
|
||||
@@ -76,10 +79,21 @@ export function enableLigatures(term: Terminal): void {
|
||||
range => [range[0], range[1]]
|
||||
);
|
||||
}
|
||||
if (loadingState === LoadingState.FAILED) {
|
||||
throw loadError || new Error('Failure while loading font');
|
||||
}
|
||||
|
||||
return [];
|
||||
return getFallbackRanges(text, fallbackLigatures);
|
||||
});
|
||||
}
|
||||
|
||||
function getFallbackRanges(text: string, fallbackLigatures: string[]): [number, number][] {
|
||||
const ranges: [number, number][] = [];
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
for (let j = 0; j < fallbackLigatures.length; j++) {
|
||||
if (text.startsWith(fallbackLigatures[j], i)) {
|
||||
ranges.push([i, i + fallbackLigatures[j].length]);
|
||||
i += fallbackLigatures[j].length - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
@@ -17,11 +17,14 @@ declare module 'xterm-addon-ligatures' {
|
||||
export class LigaturesAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Creates a new ligatures addon.
|
||||
*
|
||||
* @param options Options for the ligatures addon.
|
||||
*/
|
||||
constructor();
|
||||
constructor(options?: Partial<ILigatureOptions>);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
*
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
*/
|
||||
public activate(terminal: Terminal): void;
|
||||
@@ -31,4 +34,25 @@ declare module 'xterm-addon-ligatures' {
|
||||
*/
|
||||
public dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the ligatures addon.
|
||||
*/
|
||||
export interface ILigatureOptions {
|
||||
/**
|
||||
* Fallback ligatures to use when the font access API is either not supported by the browser or
|
||||
* access is denied. The default set of ligatures is taken from Iosevka's default "calt"
|
||||
* ligation set: https://typeof.net/Iosevka/
|
||||
*
|
||||
* ```
|
||||
* <-- <--- <<- <- -> ->> --> --->
|
||||
* <== <=== <<= <= => =>> ==> ===> >= >>=
|
||||
* <-> <--> <---> <----> <=> <==> <===> <====> -------->
|
||||
* <~~ <~ ~> ~~> :: ::: == != === !==
|
||||
* := :- :+ <* <*> *> <| <|> |> +: -: =: :>
|
||||
* ++ +++ <!-- <!--- <***>
|
||||
* ```
|
||||
*/
|
||||
fallbackLigatures: string[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ const mainFile = 'xterm-addon-ligatures.js';
|
||||
module.exports = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
target: 'electron-renderer',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
@@ -30,18 +29,19 @@ module.exports = {
|
||||
},
|
||||
mode: 'production',
|
||||
externals: {
|
||||
'font-finder': 'font-finder',
|
||||
'fs': 'fs',
|
||||
'path': 'path',
|
||||
'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,
|
||||
fs: false,
|
||||
os: false,
|
||||
path: false
|
||||
path: false,
|
||||
stream: false,
|
||||
util: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-search",
|
||||
"version": "0.9.0",
|
||||
"version": "0.12.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -21,6 +21,6 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm';
|
||||
import { Terminal, IDisposable, ITerminalAddon, IDecoration } from 'xterm';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable, disposeArray } from 'common/Lifecycle';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
@@ -29,6 +30,10 @@ export interface ISearchPosition {
|
||||
startRow: number;
|
||||
}
|
||||
|
||||
export interface ISearchAddonOptions {
|
||||
highlightLimit: number;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
@@ -47,15 +52,22 @@ type LineCacheEntry = [
|
||||
lineOffsets: number[]
|
||||
];
|
||||
|
||||
interface IHighlight extends IDisposable {
|
||||
decoration: IDecoration;
|
||||
match: ISearchResult;
|
||||
}
|
||||
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
const DEFAULT_HIGHLIGHT_LIMIT = 1000;
|
||||
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _selectedDecoration: IDecoration | undefined;
|
||||
private _resultDecorations: Map<number, IDecoration[]> | undefined;
|
||||
private _searchResults: Map<string, ISearchResult> | undefined;
|
||||
private _highlightedLines: Set<number> = new Set();
|
||||
private _highlightDecorations: IHighlight[] = [];
|
||||
private _selectedDecoration: IHighlight | undefined;
|
||||
private _highlightLimit: number;
|
||||
private _onDataDisposable: IDisposable | undefined;
|
||||
private _onResizeDisposable: IDisposable | undefined;
|
||||
private _lastSearchOptions: ISearchOptions | undefined;
|
||||
@@ -70,15 +82,24 @@ export class SearchAddon implements ITerminalAddon {
|
||||
private _cursorMoveListener: IDisposable | undefined;
|
||||
private _resizeListener: IDisposable | undefined;
|
||||
|
||||
private _resultIndex: number | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
|
||||
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
|
||||
constructor(options?: Partial<ISearchAddonOptions>) {
|
||||
super();
|
||||
|
||||
this._highlightLimit = options?.highlightLimit ?? DEFAULT_HIGHLIGHT_LIMIT;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
|
||||
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
|
||||
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
|
||||
this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches()));
|
||||
this.register(toDisposable(() => {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateMatches(): void {
|
||||
@@ -87,30 +108,18 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
if (this._cachedSearchTerm && this._lastSearchOptions?.decorations) {
|
||||
this._highlightTimeout = setTimeout(() => {
|
||||
this.findPrevious(this._cachedSearchTerm!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
this._resultIndex = this._searchResults ? this._searchResults.size - 1 : -1;
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults?.size ?? -1 });
|
||||
const term = this._cachedSearchTerm;
|
||||
this._cachedSearchTerm = undefined;
|
||||
this.findPrevious(term!, { ...this._lastSearchOptions, incremental: true, noScroll: true });
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}
|
||||
|
||||
public clearDecorations(retainCachedSearchTerm?: boolean): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._searchResults?.clear();
|
||||
this._resultDecorations?.forEach(decorations => {
|
||||
for (const d of decorations) {
|
||||
d.dispose();
|
||||
}
|
||||
});
|
||||
this._resultDecorations?.clear();
|
||||
this._searchResults = undefined;
|
||||
this._resultDecorations = undefined;
|
||||
this.clearActiveDecoration();
|
||||
disposeArray(this._highlightDecorations);
|
||||
this._highlightDecorations = [];
|
||||
this._highlightedLines.clear();
|
||||
if (!retainCachedSearchTerm) {
|
||||
this._cachedSearchTerm = undefined;
|
||||
}
|
||||
@@ -126,7 +135,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* doesn't exist, do nothing.
|
||||
* @param term The search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
* @returns Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
@@ -134,11 +143,16 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findNextAndSelect(term, searchOptions), searchOptions);
|
||||
|
||||
const found = this._findNextAndSelect(term, searchOptions);
|
||||
this._fireResults(searchOptions);
|
||||
this._cachedSearchTerm = term;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private _highlightAllMatches(term: string, searchOptions: ISearchOptions): void {
|
||||
@@ -153,32 +167,30 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
// new search, clear out the old decorations
|
||||
this.clearDecorations(true);
|
||||
this._searchResults = new Map<string, ISearchResult>();
|
||||
this._resultDecorations = new Map<number, IDecoration[]>();
|
||||
const resultDecorations = this._resultDecorations;
|
||||
|
||||
const searchResultsWithHighlight: ISearchResult[] = [];
|
||||
let prevResult: ISearchResult | undefined = undefined;
|
||||
let result = this._find(term, 0, 0, searchOptions);
|
||||
while (result && !this._searchResults.get(`${result.row}-${result.col}`)) {
|
||||
this._searchResults.set(`${result.row}-${result.col}`, result);
|
||||
while (result && (prevResult?.row !== result.row || prevResult?.col !== result.col)) {
|
||||
if (searchResultsWithHighlight.length >= this._highlightLimit) {
|
||||
break;
|
||||
}
|
||||
prevResult = result;
|
||||
searchResultsWithHighlight.push(prevResult);
|
||||
result = this._find(
|
||||
term,
|
||||
result.col + result.term.length >= this._terminal.cols ? result.row + 1 : result.row,
|
||||
result.col + result.term.length >= this._terminal.cols ? 0 : result.col + 1,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
|
||||
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
|
||||
searchOptions
|
||||
);
|
||||
if (this._searchResults.size > 1000) {
|
||||
this.clearDecorations();
|
||||
this._resultIndex = undefined;
|
||||
return;
|
||||
}
|
||||
for (const match of searchResultsWithHighlight) {
|
||||
const decoration = this._createResultDecoration(match, searchOptions.decorations!);
|
||||
if (decoration) {
|
||||
this._highlightedLines.add(decoration.marker.line);
|
||||
this._highlightDecorations.push({ decoration, match, dispose() { decoration.dispose(); } });
|
||||
}
|
||||
}
|
||||
this._searchResults.forEach(result => {
|
||||
const resultDecoration = this._createResultDecoration(result, searchOptions.decorations!);
|
||||
if (resultDecoration) {
|
||||
const decorationsForLine = resultDecorations.get(resultDecoration.marker.line) || [];
|
||||
decorationsForLine.push(resultDecoration);
|
||||
resultDecorations.set(resultDecoration.marker.line, decorationsForLine);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _find(term: string, startRow: number, startCol: number, searchOptions?: ISearchOptions): ISearchResult | undefined {
|
||||
@@ -223,26 +235,22 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._cachedSearchTerm = undefined;
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
const prevSelectedPos = this._terminal.getSelectionPosition();
|
||||
this._terminal.clearSelection();
|
||||
|
||||
let startCol = 0;
|
||||
let startRow = 0;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
// Start from the selection end if there is a selection
|
||||
// For incremental search, use existing row
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
|
||||
startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
|
||||
if (prevSelectedPos) {
|
||||
if (this._cachedSearchTerm === term) {
|
||||
startCol = prevSelectedPos.end.x;
|
||||
startRow = prevSelectedPos.end.y;
|
||||
} else {
|
||||
startCol = prevSelectedPos.start.x;
|
||||
startRow = prevSelectedPos.start.y;
|
||||
}
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
@@ -281,24 +289,12 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
// If there is only one result, wrap back and return selection if it exists.
|
||||
if (!result && currentSelection) {
|
||||
searchPosition.startRow = currentSelection.startRow;
|
||||
if (!result && prevSelectedPos) {
|
||||
searchPosition.startRow = prevSelectedPos.start.y;
|
||||
searchPosition.startCol = 0;
|
||||
result = this._findInLine(term, searchPosition, searchOptions);
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined) {
|
||||
this._resultIndex = 0;
|
||||
} else {
|
||||
this._resultIndex++;
|
||||
if (this._resultIndex >= this._searchResults.size) {
|
||||
this._resultIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
@@ -307,7 +303,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* doesn't exist, do nothing.
|
||||
* @param term The search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
* @returns Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
@@ -315,75 +311,74 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
this._lastSearchOptions = searchOptions;
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined || this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
if (this._cachedSearchTerm === undefined || term !== this._cachedSearchTerm) {
|
||||
this._highlightAllMatches(term, searchOptions);
|
||||
}
|
||||
}
|
||||
return this._fireResults(term, this._findPreviousAndSelect(term, searchOptions), searchOptions);
|
||||
|
||||
const found = this._findPreviousAndSelect(term, searchOptions);
|
||||
this._fireResults(searchOptions);
|
||||
this._cachedSearchTerm = term;
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
private _fireResults(term: string, found: boolean, searchOptions?: ISearchOptions): boolean {
|
||||
private _fireResults(searchOptions?: ISearchOptions): void {
|
||||
if (searchOptions?.decorations) {
|
||||
if (this._resultIndex !== undefined && this._searchResults?.size !== undefined) {
|
||||
this._onDidChangeResults.fire({ resultIndex: this._resultIndex, resultCount: this._searchResults.size });
|
||||
} else {
|
||||
this._onDidChangeResults.fire(undefined);
|
||||
let resultIndex = -1;
|
||||
if (this._selectedDecoration) {
|
||||
const selectedMatch = this._selectedDecoration.match;
|
||||
for (let i = 0; i < this._highlightDecorations.length; i++) {
|
||||
const match = this._highlightDecorations[i].match;
|
||||
if (match.row === selectedMatch.row && match.col === selectedMatch.col && match.size === selectedMatch.size) {
|
||||
resultIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._onDidChangeResults.fire({ resultIndex, resultCount: this._highlightDecorations.length });
|
||||
}
|
||||
this._cachedSearchTerm = term;
|
||||
return found;
|
||||
}
|
||||
|
||||
private _findPreviousAndSelect(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot use addon until it has been loaded');
|
||||
}
|
||||
let result: ISearchResult | undefined;
|
||||
if (!this._terminal || !term || term.length === 0) {
|
||||
result = undefined;
|
||||
this._terminal?.clearSelection();
|
||||
this.clearDecorations();
|
||||
this._resultIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
this._resultIndex = undefined;
|
||||
this._terminal.clearSelection();
|
||||
}
|
||||
const prevSelectedPos = this._terminal.getSelectionPosition();
|
||||
this._terminal.clearSelection();
|
||||
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows;
|
||||
let startRow = this._terminal.buffer.active.baseY + this._terminal.rows - 1;
|
||||
let startCol = this._terminal.cols;
|
||||
const isReverseSearch = true;
|
||||
|
||||
const incremental = searchOptions ? searchOptions.incremental : false;
|
||||
let currentSelection: ISelectionPosition | undefined;
|
||||
if (this._terminal.hasSelection()) {
|
||||
currentSelection = this._terminal.getSelectionPosition()!;
|
||||
// Start from selection start if there is a selection
|
||||
startRow = currentSelection.startRow;
|
||||
startCol = currentSelection.startColumn;
|
||||
}
|
||||
|
||||
this._initLinesCache();
|
||||
const searchPosition: ISearchPosition = {
|
||||
startRow,
|
||||
startCol
|
||||
};
|
||||
|
||||
if (incremental) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
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;
|
||||
let result: ISearchResult | undefined;
|
||||
if (prevSelectedPos) {
|
||||
searchPosition.startRow = startRow = prevSelectedPos.start.y;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.start.x;
|
||||
if (this._cachedSearchTerm !== term) {
|
||||
// Try to expand selection to right first.
|
||||
result = this._findInLine(term, searchPosition, searchOptions, false);
|
||||
if (!result) {
|
||||
// If selection was not able to be expanded to the right, then try reverse search
|
||||
searchPosition.startRow = startRow = prevSelectedPos.end.y;
|
||||
searchPosition.startCol = startCol = prevSelectedPos.end.x;
|
||||
}
|
||||
result = this._findInLine(term, searchPosition, searchOptions, true);
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
}
|
||||
|
||||
@@ -399,8 +394,8 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
// If we hit the top and didn't search from the very bottom wrap back down
|
||||
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows)) {
|
||||
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows); y >= startRow; y--) {
|
||||
if (!result && startRow !== (this._terminal.buffer.active.baseY + this._terminal.rows - 1)) {
|
||||
for (let y = (this._terminal.buffer.active.baseY + this._terminal.rows - 1); y >= startRow; y--) {
|
||||
searchPosition.startRow = y;
|
||||
result = this._findInLine(term, searchPosition, searchOptions, isReverseSearch);
|
||||
if (result) {
|
||||
@@ -409,22 +404,6 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
|
||||
if (this._searchResults) {
|
||||
if (this._searchResults.size === 0) {
|
||||
this._resultIndex = -1;
|
||||
} else if (this._resultIndex === undefined || this._resultIndex < 0) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
} else {
|
||||
this._resultIndex--;
|
||||
if (this._resultIndex === -1) {
|
||||
this._resultIndex = this._searchResults.size - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If there is only one result, return true.
|
||||
if (!result && currentSelection) return true;
|
||||
|
||||
// Set selection and scroll if a result was found
|
||||
return this._selectResult(result, searchOptions?.decorations, searchOptions?.noScroll);
|
||||
}
|
||||
@@ -477,10 +456,10 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||
* text starts on is searched.
|
||||
* @param term The search term.
|
||||
* @param position The position to start the search.
|
||||
* @param searchPosition The position to start the search.
|
||||
* @param searchOptions Search options.
|
||||
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
|
||||
* @return The search result if it was found.
|
||||
* @returns The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
||||
const terminal = this._terminal!;
|
||||
@@ -627,7 +606,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
* function is useful for getting the actual text underneath the raw selection
|
||||
* position.
|
||||
* @param line The line being translated.
|
||||
* @param lineIndex The index of the line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
*/
|
||||
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
|
||||
@@ -662,7 +641,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
* @return Whether a result was selected.
|
||||
* @returns Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
const terminal = this._terminal!;
|
||||
@@ -675,7 +654,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
if (options) {
|
||||
const marker = terminal.registerMarker(-terminal.buffer.active.baseY - terminal.buffer.active.cursorY + result.row);
|
||||
if (marker) {
|
||||
this._selectedDecoration = terminal.registerDecoration({
|
||||
const decoration = terminal.registerDecoration({
|
||||
marker,
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
@@ -685,8 +664,13 @@ export class SearchAddon implements ITerminalAddon {
|
||||
color: options.activeMatchColorOverviewRuler
|
||||
}
|
||||
});
|
||||
this._selectedDecoration?.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true));
|
||||
this._selectedDecoration?.onDispose(() => marker.dispose());
|
||||
if (decoration) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(marker);
|
||||
disposables.push(decoration.onRender((e) => this._applyStyles(e, options.activeMatchBorder, true)));
|
||||
disposables.push(decoration.onDispose(() => disposeArray(disposables)));
|
||||
this._selectedDecoration = { decoration, match: result, dispose() { decoration.dispose(); } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,16 +686,13 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies styles to the decoration when it is rendered
|
||||
* @param element the decoration's element
|
||||
* @param backgroundColor the background color to apply
|
||||
* @param borderColor the border color to apply
|
||||
* Applies styles to the decoration when it is rendered.
|
||||
* @param element The decoration's element.
|
||||
* @param borderColor The border color to apply.
|
||||
* @param isActiveResult Whether the element is part of the active search result.
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
if (element.clientWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
if (!element.classList.contains('xterm-find-result-decoration')) {
|
||||
element.classList.add('xterm-find-result-decoration');
|
||||
if (borderColor) {
|
||||
@@ -740,13 +721,17 @@ export class SearchAddon implements ITerminalAddon {
|
||||
x: result.col,
|
||||
width: result.size,
|
||||
backgroundColor: options.matchBackground,
|
||||
overviewRulerOptions: this._resultDecorations?.get(marker.line) ? undefined : {
|
||||
overviewRulerOptions: this._highlightedLines.has(marker.line) ? undefined : {
|
||||
color: options.matchOverviewRuler,
|
||||
position: 'center'
|
||||
}
|
||||
});
|
||||
findResultDecoration?.onRender((e) => this._applyStyles(e, options.matchBorder, false));
|
||||
findResultDecoration?.onDispose(() => marker.dispose());
|
||||
if (findResultDecoration) {
|
||||
const disposables: IDisposable[] = [];
|
||||
disposables.push(marker);
|
||||
disposables.push(findResultDecoration.onRender((e) => this._applyStyles(e, options.matchBorder, false)));
|
||||
disposables.push(findResultDecoration.onDispose(() => disposeArray(disposables)));
|
||||
}
|
||||
return findResultDecoration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
|
||||
@@ -60,35 +60,35 @@ describe('Search Tests', function (): void {
|
||||
await page.evaluate(`window.term.writeln('package.jsonc\\n')`);
|
||||
await writeSync(page, 'package.json pack package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
|
||||
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
|
||||
});
|
||||
it('Incremental Find Next', async () => {
|
||||
await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`);
|
||||
await writeSync(page, 'package.jsonc');
|
||||
await page.evaluate(`window.search.findNext('pack', {incremental: true})`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
|
||||
let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
// We look further ahead in the line to ensure that pack was selected from package.lock
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock');
|
||||
await page.evaluate(`window.search.findNext('package.j', {incremental: true})`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json');
|
||||
await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`);
|
||||
// We have to reevaluate line because it should have switched starting rows at this point
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`);
|
||||
line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc');
|
||||
assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc');
|
||||
});
|
||||
it('Simple Regex', async () => {
|
||||
await writeSync(page, 'abc123defABCD');
|
||||
@@ -116,10 +116,14 @@ describe('Search Tests', function (): void {
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '𝄞');
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true);
|
||||
assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), {
|
||||
startRow: 0,
|
||||
endRow: 0,
|
||||
startColumn: 7,
|
||||
endColumn: 8
|
||||
start: {
|
||||
x: 7,
|
||||
y: 0
|
||||
},
|
||||
end: {
|
||||
x: 8,
|
||||
y: 0
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,7 +178,7 @@ describe('Search Tests', function (): void {
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
await writeSync(page, 'd abc aabc d');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
@@ -197,15 +201,64 @@ describe('Search Tests', function (): void {
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findNext('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire with more than 1k matches', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
|
||||
await writeSync(page, data);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 },
|
||||
{ resultCount: 1000, resultIndex: 1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: 0 },
|
||||
{ resultCount: 1000, resultIndex: 1 },
|
||||
{ resultCount: 1000, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
it('should fire when writing to terminal', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
|
||||
assert.strictEqual(await page.evaluate(`window.search.findNext('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
await writeSync(page, 'abc bc c\\n\\r');
|
||||
await timeout(300);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 3, resultIndex: 0 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
describe('findPrevious', () => {
|
||||
it('should not fire unless the decorations option is set', async () => {
|
||||
@@ -229,6 +282,7 @@ describe('Search Tests', function (): void {
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 }
|
||||
]);
|
||||
await page.evaluate(`window.term.clearSelection()`);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('b', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1, resultIndex: 0 },
|
||||
@@ -258,7 +312,7 @@ describe('Search Tests', function (): void {
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc aabc');
|
||||
await writeSync(page, 'd abc aabc d');
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('a', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 }
|
||||
@@ -281,15 +335,64 @@ describe('Search Tests', function (): void {
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('d', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
assert.deepStrictEqual(await page.evaluate(`window.search.findPrevious('abcd', { incremental: true, decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), false);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 3, resultIndex: 2 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 2, resultIndex: 0 },
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 0, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire with more than 1k matches', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
const data = ('a bc'.repeat(10) + '\\n\\r').repeat(150);
|
||||
await writeSync(page, data);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('a', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('bc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 },
|
||||
{ resultCount: 1000, resultIndex: -1 }
|
||||
]);
|
||||
});
|
||||
it('should fire when writing to terminal', async () => {
|
||||
await page.evaluate(`
|
||||
window.calls = [];
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
await writeSync(page, 'abc bc c\\n\\r'.repeat(2));
|
||||
assert.strictEqual(await page.evaluate(`window.search.findPrevious('abc', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
]);
|
||||
await writeSync(page, 'abc bc c\\n\\r');
|
||||
await timeout(300);
|
||||
assert.deepStrictEqual(await page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 },
|
||||
{ resultCount: 3, resultIndex: 1 }
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -313,69 +416,70 @@ describe('Search Tests', function (): void {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
|
||||
});
|
||||
it('should find all occurrences using findPrevious', async () => {
|
||||
|
||||
it('should y all occurrences using findPrevious', async () => {
|
||||
await writeSync(page, fixture);
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } });
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } });
|
||||
// Wrap around to first result
|
||||
assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true);
|
||||
selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`);
|
||||
assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 });
|
||||
assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es6",
|
||||
],
|
||||
|
||||
+20
-4
@@ -75,10 +75,28 @@ declare module 'xterm-addon-search' {
|
||||
activeMatchColorOverviewRuler: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for the search addon.
|
||||
*/
|
||||
export interface ISearchAddonOptions {
|
||||
/**
|
||||
* Max number of matches highlighted when decorations are enabled.
|
||||
* Defaults to 1000 highlighted matches
|
||||
*/
|
||||
highlightLimit: number
|
||||
}
|
||||
|
||||
/**
|
||||
* An xterm.js addon that provides search functionality.
|
||||
*/
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
/**
|
||||
* Creates a new search addon.
|
||||
* @param options Options for the search addon.
|
||||
*/
|
||||
constructor(options?: Partial<ISearchAddonOptions>);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
* @param terminal The terminal the addon is being loaded in.
|
||||
@@ -121,10 +139,8 @@ declare module 'xterm-addon-search' {
|
||||
/**
|
||||
* When decorations are enabled, fires when
|
||||
* the search results change.
|
||||
* @returns -1 for resultIndex for a resultCount of 0
|
||||
* and @returns undefined when the threshold of 1k results
|
||||
* is exceeded and decorations are disposed of.
|
||||
* @returns -1 for resultIndex when the threshold of matches is exceeded.
|
||||
*/
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number } | undefined>;
|
||||
readonly onDidChangeResults: IEvent<{ resultIndex: number, resultCount: number }>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.7.0",
|
||||
"version": "0.10.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -24,6 +24,6 @@
|
||||
"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"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,9 +7,10 @@ import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
@@ -44,14 +45,11 @@ class TestSelectionService {
|
||||
}
|
||||
|
||||
describe('xterm-addon-serialize', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
let serializeAddon: SerializeAddon;
|
||||
let terminal: Terminal;
|
||||
let selectionService: any;
|
||||
|
||||
before(() => {
|
||||
serializeAddon = new SerializeAddon();
|
||||
@@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => {
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
@@ -74,13 +71,11 @@ describe('xterm-addon-serialize', () => {
|
||||
}
|
||||
});
|
||||
|
||||
terminal = new Terminal({ cols: 10, rows: 2 });
|
||||
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
|
||||
terminal.loadAddon(serializeAddon);
|
||||
|
||||
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
cm = new ColorManager(document, false);
|
||||
(terminal as any)._core._colorManager = cm;
|
||||
(terminal as any)._core._selectionService = selectionService;
|
||||
(terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
|
||||
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
|
||||
@@ -77,6 +77,7 @@ function equalFlags(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): bo
|
||||
return cell1.isInverse() === cell2.isInverse()
|
||||
&& cell1.isBold() === cell2.isBold()
|
||||
&& cell1.isUnderline() === cell2.isUnderline()
|
||||
&& cell1.isOverline() === cell2.isOverline()
|
||||
&& cell1.isBlink() === cell2.isBlink()
|
||||
&& cell1.isInvisible() === cell2.isInvisible()
|
||||
&& cell1.isItalic() === cell2.isItalic()
|
||||
@@ -264,6 +265,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
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.isOverline() !== oldCell.isOverline()) { sgrSeq.push(cell.isOverline() ? 53 : 55); }
|
||||
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); }
|
||||
@@ -443,8 +445,8 @@ export class SerializeAddon implements ITerminalAddon {
|
||||
const selection = this._terminal?.getSelectionPosition();
|
||||
if (selection !== undefined) {
|
||||
return handler.serialize({
|
||||
start: { x: selection.startRow, y: selection.startColumn },
|
||||
end: { x: selection.endRow, y: selection.endColumn }
|
||||
start: { x: selection.start.y, y: selection.start.x },
|
||||
end: { x: selection.end.y, y: selection.end.x }
|
||||
});
|
||||
}
|
||||
|
||||
@@ -544,7 +546,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
super(buffer);
|
||||
|
||||
// https://github.com/xtermjs/xterm.js/issues/3601
|
||||
this._colors = (_terminal as any)._core._colorManager.colors;
|
||||
this._colors = (_terminal as any)._core._themeService.colors;
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
@@ -625,7 +627,9 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
|
||||
if (cell.isInverse()) { content.push('color: #000000; background-color: #BFBFBF;'); }
|
||||
if (cell.isBold()) { content.push('font-weight: bold;'); }
|
||||
if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
if (cell.isUnderline() && cell.isOverline()) { content.push('text-decoration: overline underline;'); }
|
||||
else if (cell.isUnderline()) { content.push('text-decoration: underline;'); }
|
||||
else if (cell.isOverline()) { content.push('text-decoration: overline;'); }
|
||||
if (cell.isBlink()) { content.push('text-decoration: blink;'); }
|
||||
if (cell.isInvisible()) { content.push('visibility: hidden;'); }
|
||||
if (cell.isItalic()) { content.push('font-style: italic;'); }
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('SerializeAddon', () => {
|
||||
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 openTerminal(page, { rows: 10, cols: 10 });
|
||||
await page.evaluate(`
|
||||
window.serializeAddon = new SerializeAddon();
|
||||
window.term.loadAddon(window.serializeAddon);
|
||||
@@ -220,6 +220,18 @@ describe('SerializeAddon', () => {
|
||||
assert.equal(await page.evaluate(`serializeAddon.serialize();`), lines.join('\r\n'));
|
||||
});
|
||||
|
||||
it('serialize all rows of content with overline', async () => {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(OVERLINED) + line, // Overlined
|
||||
sgr(UNDERLINED) + line, // Overlined, Underlined
|
||||
sgr(NORMAL) + line // 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 separately', async function(): Promise<any> {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
@@ -601,6 +613,7 @@ const BLINK = '5';
|
||||
const INVERSE = '7';
|
||||
const INVISIBLE = '8';
|
||||
const STRIKETHROUGH = '9';
|
||||
const OVERLINED = '53';
|
||||
|
||||
const NO_BOLD = '22';
|
||||
const NO_DIM = '22';
|
||||
@@ -610,3 +623,4 @@ const NO_BLINK = '25';
|
||||
const NO_INVERSE = '27';
|
||||
const NO_INVISIBLE = '28';
|
||||
const NO_STRIKETHROUGH = '29';
|
||||
const NO_OVERLINED = '55';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-unicode11",
|
||||
"version": "0.3.0",
|
||||
"version": "0.5.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -21,6 +21,6 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { IUnicodeVersionProvider } from 'xterm';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
type CharWidth = 0 | 1 | 2;
|
||||
|
||||
@@ -198,15 +197,15 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
|
||||
constructor() {
|
||||
if (!table) {
|
||||
table = new Uint8Array(65536);
|
||||
fill(table, 1);
|
||||
table.fill(1);
|
||||
table[0] = 0;
|
||||
fill(table, 0, 1, 32);
|
||||
fill(table, 0, 0x7f, 0xa0);
|
||||
table.fill(0, 1, 32);
|
||||
table.fill(0, 0x7f, 0xa0);
|
||||
for (let r = 0; r < BMP_COMBINING.length; ++r) {
|
||||
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
|
||||
table.fill(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);
|
||||
table.fill(2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-web-links",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -21,6 +21,7 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange, IBufferLine } from 'xterm';
|
||||
|
||||
export interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
@@ -45,55 +45,51 @@ 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);
|
||||
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
||||
const line = lines.join('');
|
||||
|
||||
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;
|
||||
while (match = rex.exec(line)) {
|
||||
const text = match[0];
|
||||
|
||||
// check via URL if the matched text would form a proper url
|
||||
// NOTE: This outsources the ugly url parsing to the browser.
|
||||
// To avoid surprising auto expansion from URL we additionally
|
||||
// check afterwards if the provided string resembles the parsed
|
||||
// one close enough:
|
||||
// - decodeURI decode path segement back to byte repr
|
||||
// to detect unicode auto conversion correctly
|
||||
// - append / also match domain urls w'o any path notion
|
||||
try {
|
||||
const url = new URL(text);
|
||||
const urlText = decodeURI(url.toString());
|
||||
if (text !== urlText && text + '/' !== urlText) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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++;
|
||||
}
|
||||
|
||||
let startX = stringIndex + 1;
|
||||
let startY = startLineIndex + 1;
|
||||
while (startX > terminal.cols) {
|
||||
startX -= terminal.cols;
|
||||
startY++;
|
||||
// map string positions back to buffer positions
|
||||
// values are 0-based right side excluding
|
||||
const [startY, startX] = LinkComputer._mapStrIdx(terminal, startLineIndex, 0, match.index);
|
||||
const [endY, endX] = LinkComputer._mapStrIdx(terminal, startY, startX, text.length);
|
||||
|
||||
if (startY === -1 || startX === -1 || endY === -1 || endX === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// range expects values 1-based right side including, thus +1 except for endX
|
||||
const range = {
|
||||
start: {
|
||||
x: startX,
|
||||
y: startY
|
||||
x: startX + 1,
|
||||
y: startY + 1
|
||||
},
|
||||
end: {
|
||||
x: endX,
|
||||
y: endY
|
||||
y: endY + 1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,42 +100,99 @@ export class LinkComputer {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Get wrapped content lines for the current line index.
|
||||
* The top/bottom line expansion stops at whitespaces or length > 2048.
|
||||
* Returns an array with line strings and the top line index.
|
||||
*
|
||||
* NOTE: We pull line strings with trimRight=true on purpose to make sure
|
||||
* to correctly match urls with early wrapped wide chars. This corrupts the string index
|
||||
* for 1:1 backmapping to buffer positions, thus needs an additional correction in _mapStrIdx.
|
||||
*/
|
||||
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
|
||||
let lineString = '';
|
||||
let lineWrapsToNext: boolean;
|
||||
let prevLinesToWrap: boolean;
|
||||
private static _getWindowedLineStrings(lineIndex: number, terminal: Terminal): [string[], number] {
|
||||
let line: IBufferLine | undefined;
|
||||
let topIdx = lineIndex;
|
||||
let bottomIdx = lineIndex;
|
||||
let length = 0;
|
||||
let content = '';
|
||||
const lines: string[] = [];
|
||||
|
||||
do {
|
||||
const line = terminal.buffer.active.getLine(lineIndex);
|
||||
if ((line = terminal.buffer.active.getLine(lineIndex))) {
|
||||
const currentContent = line.translateToString(true);
|
||||
|
||||
// expand top, stop on whitespaces or length > 2048
|
||||
if (line.isWrapped && currentContent[0] !== ' ') {
|
||||
length = 0;
|
||||
while ((line = terminal.buffer.active.getLine(--topIdx)) && length < 2048) {
|
||||
content = line.translateToString(true);
|
||||
length += content.length;
|
||||
lines.push(content);
|
||||
if (!line.isWrapped || content.indexOf(' ') !== -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines.reverse();
|
||||
}
|
||||
|
||||
// append current line
|
||||
lines.push(currentContent);
|
||||
|
||||
// expand bottom, stop on whitespaces or length > 2048
|
||||
length = 0;
|
||||
while ((line = terminal.buffer.active.getLine(++bottomIdx)) && line.isWrapped && length < 2048) {
|
||||
content = line.translateToString(true);
|
||||
length += content.length;
|
||||
lines.push(content);
|
||||
if (content.indexOf(' ') !== -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [lines, topIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a string index back to buffer positions.
|
||||
* Returns buffer position as [lineIndex, columnIndex] 0-based,
|
||||
* or [-1, -1] in case the lookup ran into a non-existing line.
|
||||
*/
|
||||
private static _mapStrIdx(terminal: Terminal, lineIndex: number, rowIndex: number, stringIndex: number): [number, number] {
|
||||
const buf = terminal.buffer.active;
|
||||
const cell = buf.getNullCell();
|
||||
let start = rowIndex;
|
||||
while (stringIndex) {
|
||||
const line = buf.getLine(lineIndex);
|
||||
if (!line) {
|
||||
break;
|
||||
return [-1, -1];
|
||||
}
|
||||
for (let i = start; i < line.length; ++i) {
|
||||
line.getCell(i, cell);
|
||||
const chars = cell.getChars();
|
||||
const width = cell.getWidth();
|
||||
if (width) {
|
||||
stringIndex -= chars.length || 1;
|
||||
|
||||
if (line.isWrapped) {
|
||||
lineIndex--;
|
||||
// correct stringIndex for early wrapped wide chars:
|
||||
// - currently only happens at last cell
|
||||
// - cells to the right are reset with chars='' and width=1 in InputHandler.print
|
||||
// - follow-up line must be wrapped and contain wide char at first cell
|
||||
// --> if all these conditions are met, correct stringIndex by +1
|
||||
if (i === line.length - 1 && chars === '') {
|
||||
const line = buf.getLine(lineIndex + 1);
|
||||
if (line && line.isWrapped) {
|
||||
line.getCell(0, cell);
|
||||
if (cell.getWidth() === 2) {
|
||||
stringIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stringIndex < 0) {
|
||||
return [lineIndex, i];
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
start = 0;
|
||||
}
|
||||
return [lineIndex, start];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,28 +3,22 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,18})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+@]*)*([^:"\'\\s])';
|
||||
const pathClause = '(' + pathCharacterSet + ')?';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
// consider everthing starting with http:// or https://
|
||||
// up to first whitespace, `"` or `'` as url
|
||||
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
||||
// resembling the old (...)*([^:"\'\\s]) final path clause
|
||||
// additionally exclude early + final:
|
||||
// - unsafe from rfc3986: !*'()
|
||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
|
||||
// also exclude as finals:
|
||||
// - final interpunction like ,.!?
|
||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||
// - unsafe chars from rfc1738: {}|\^~[]`
|
||||
const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
|
||||
|
||||
|
||||
function handleLink(event: MouseEvent, uri: string): void {
|
||||
const newWindow = window.open();
|
||||
@@ -41,37 +35,23 @@ function handleLink(event: MouseEvent, uri: string): void {
|
||||
}
|
||||
|
||||
export class WebLinksAddon implements ITerminalAddon {
|
||||
private _linkMatcherId: number | undefined;
|
||||
private _terminal: Terminal | undefined;
|
||||
private _linkProvider: IDisposable | undefined;
|
||||
|
||||
constructor(
|
||||
private _handler: (event: MouseEvent, uri: string) => void = handleLink,
|
||||
private _options: ILinkMatcherOptions | ILinkProviderOptions = {},
|
||||
private _useLinkProvider: boolean = false
|
||||
private _options: ILinkProviderOptions = {}
|
||||
) {
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
|
||||
if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) {
|
||||
const options = this._options as ILinkProviderOptions;
|
||||
const regex = options.urlRegex || strictUrlRegex;
|
||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
||||
} else {
|
||||
// TODO: This should be removed eventually
|
||||
const options = this._options as ILinkMatcherOptions;
|
||||
options.matchIndex = 1;
|
||||
this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options);
|
||||
}
|
||||
const options = this._options as ILinkProviderOptions;
|
||||
const regex = options.urlRegex || strictUrlRegex;
|
||||
this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._linkMatcherId !== undefined && this._terminal !== undefined) {
|
||||
this._terminal.deregisterLinkMatcher(this._linkMatcherId);
|
||||
}
|
||||
|
||||
this._linkProvider?.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es2015"
|
||||
|
||||
@@ -14,6 +14,22 @@ let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
|
||||
interface ILinkStateData {
|
||||
uri?: string;
|
||||
range?: {
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
describe('WebLinksAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
@@ -35,10 +51,47 @@ describe('WebLinksAddon', () => {
|
||||
it('.io', async function(): Promise<any> {
|
||||
await testHostName('foo.io');
|
||||
});
|
||||
|
||||
describe('correct buffer offsets & uri', () => {
|
||||
it('all half width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
|
||||
await resetAndHover(5, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
|
||||
});
|
||||
it('url after full width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
|
||||
await resetAndHover(8, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
|
||||
});
|
||||
it('full width within url and before', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
|
||||
await resetAndHover(8, 1);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(17, 2);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
|
||||
});
|
||||
it('name + password url after full width and combining', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
|
||||
await resetAndHover(12, 1);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
await resetAndHover(13, 2);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function testHostName(hostname: string): Promise<void> {
|
||||
await openTerminal(page, { rendererType: 'dom', cols: 40 });
|
||||
await openTerminal(page, { cols: 40 });
|
||||
await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`);
|
||||
const data = ` http://${hostname} \\r\\n` +
|
||||
` http://${hostname}/a~b#c~d?e~f \\r\\n` +
|
||||
@@ -64,3 +117,23 @@ 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})`));
|
||||
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
|
||||
}
|
||||
|
||||
async function setupCustom(): Promise<void> {
|
||||
await openTerminal(page, { cols: 40 });
|
||||
await page.evaluate(`window._linkStateData = {};
|
||||
window._linkaddon = new window.WebLinksAddon();
|
||||
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
|
||||
window.term.loadAddon(window._linkaddon);`);
|
||||
}
|
||||
|
||||
async function resetAndHover(col: number, row: number): Promise<void> {
|
||||
await page.evaluate(`window._linkStateData = {};`);
|
||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||
await page.hover(`${rowSelector} > :nth-child(${col})`);
|
||||
}
|
||||
|
||||
async function evalLinkStateData(uri: string, range: any): Promise<void> {
|
||||
const data: ILinkStateData = await page.evaluate(`window._linkStateData`);
|
||||
assert.equal(data.uri, uri);
|
||||
assert.deepEqual(data.range, range);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es2015"
|
||||
],
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
|
||||
import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm';
|
||||
import { Terminal, ITerminalAddon, IViewportRange } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-web-links' {
|
||||
/**
|
||||
@@ -14,13 +14,9 @@ declare module 'xterm-addon-web-links' {
|
||||
/**
|
||||
* Creates a new web links addon.
|
||||
* @param handler The callback when the link is called.
|
||||
* @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.
|
||||
* @param options Options for the link provider.
|
||||
*/
|
||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean);
|
||||
constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions);
|
||||
|
||||
/**
|
||||
* Activates the addon
|
||||
@@ -39,8 +35,7 @@ declare module 'xterm-addon-web-links' {
|
||||
*/
|
||||
export interface ILinkProviderOptions {
|
||||
/**
|
||||
* A callback that fires when the mouse hovers over a link for a period of
|
||||
* time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}).
|
||||
* A callback that fires when the mouse hovers over a link.
|
||||
*/
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
|
||||
@@ -50,7 +45,7 @@ declare module 'xterm-addon-web-links' {
|
||||
*/
|
||||
leave?(event: MouseEvent, text: string): void;
|
||||
|
||||
/**
|
||||
/**
|
||||
* A callback to use instead of the default one.
|
||||
*/
|
||||
urlRegex?: RegExp;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Terminal } from 'xterm';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
||||
const terminal = new Terminal();
|
||||
terminal.open(element);
|
||||
terminal.loadAddon(new WebglAddon());
|
||||
```
|
||||
|
||||
@@ -34,3 +35,7 @@ terminal.loadAddon(addon);
|
||||
```
|
||||
|
||||
Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost).
|
||||
|
||||
### See also
|
||||
|
||||
- [xterm-addon-canvas](https://www.npmjs.com/package/xterm-addon-canvas) A renderer for xterm.js that uses a 2d canvas that can be used as a fallback when WebGL is not available
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.12.0",
|
||||
"version": "0.15.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
@@ -22,6 +22,7 @@
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^4.0.0"
|
||||
"xterm": "^5.0.0"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IBufferLine } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
@@ -29,8 +29,9 @@ const enum VertexAttribLocations {
|
||||
CELL_POSITION = 1,
|
||||
OFFSET = 2,
|
||||
SIZE = 3,
|
||||
TEXCOORD = 4,
|
||||
TEXSIZE = 5
|
||||
TEXPAGE = 4,
|
||||
TEXCOORD = 5,
|
||||
TEXSIZE = 6
|
||||
}
|
||||
|
||||
const vertexShaderSource = `#version 300 es
|
||||
@@ -38,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
|
||||
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
|
||||
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
|
||||
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
|
||||
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
|
||||
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
|
||||
|
||||
@@ -45,43 +47,59 @@ uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec2 v_texcoord;
|
||||
flat out int v_texpage;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_texpage = int(a_texpage);
|
||||
v_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
|
||||
let textureConditionals = '';
|
||||
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
|
||||
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
|
||||
}
|
||||
return (`#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
flat in int v_texpage;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_texture, v_texcoord);
|
||||
}`;
|
||||
if (v_texpage == 0) {
|
||||
outColor = texture(u_texture[0], v_texcoord);
|
||||
} ${textureConditionals}
|
||||
}`);
|
||||
}
|
||||
|
||||
const INDICES_PER_CELL = 10;
|
||||
const INDICES_PER_CELL = 11;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
export class GlyphRenderer {
|
||||
private _atlas: WebglCharAtlas | undefined;
|
||||
// Work variables to avoid garbage collection
|
||||
let $i = 0;
|
||||
let $glyph: IRasterizedGlyph | undefined = undefined;
|
||||
let $leftCellPadding = 0;
|
||||
let $clippedPixels = 0;
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _textureLocation: WebGLUniformLocation;
|
||||
private _atlasTexture: WebGLTexture;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
export class GlyphRenderer extends Disposable {
|
||||
private readonly _program: WebGLProgram;
|
||||
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private readonly _projectionLocation: WebGLUniformLocation;
|
||||
private readonly _resolutionLocation: WebGLUniformLocation;
|
||||
private readonly _textureLocation: WebGLUniformLocation;
|
||||
private readonly _atlasTextures: GLTexture[];
|
||||
private readonly _attributesBuffer: WebGLBuffer;
|
||||
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
private _activeBuffer: number = 0;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(0),
|
||||
attributesBuffers: [
|
||||
@@ -91,14 +109,23 @@ export class GlyphRenderer {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
super();
|
||||
|
||||
const gl = this._gl;
|
||||
const program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
this._program = program;
|
||||
|
||||
if (TextureAtlas.maxAtlasPages === undefined) {
|
||||
// Typically 8 or 16
|
||||
TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
|
||||
// Almost all clients will support >= 4096
|
||||
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
|
||||
}
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
|
||||
this.register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
@@ -112,20 +139,24 @@ export class GlyphRenderer {
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
const unitQuadVerticesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
|
||||
// triangle strip
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
|
||||
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.OFFSET);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.OFFSET, 2, gl.FLOAT, false, BYTES_PER_CELL, 0);
|
||||
@@ -133,118 +164,155 @@ export class GlyphRenderer {
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup empty texture atlas
|
||||
this._atlasTexture = throwIfFalsy(gl.createTexture());
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
// Setup static uniforms
|
||||
gl.useProgram(this._program);
|
||||
const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
|
||||
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
|
||||
textureUnits[i] = i;
|
||||
}
|
||||
gl.uniform1iv(this._textureLocation, textureUnits);
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
|
||||
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
|
||||
// is ever drawn it will show characters as red rectangles.
|
||||
this._atlasTextures = [];
|
||||
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
|
||||
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
|
||||
this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
||||
this._atlasTextures[i] = glTexture;
|
||||
}
|
||||
|
||||
// Allow drawing of transparent texture
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Set viewport
|
||||
this.onResize();
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
return this._atlas ? this._atlas.beginFrame() : true;
|
||||
}
|
||||
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, chars: string): void {
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, chars);
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
||||
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
||||
// each glyph.
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg);
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, chars?: string): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
const i = (y * terminal.cols + x) * INDICES_PER_CELL;
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null character, allow space character to continue as it may have
|
||||
// underline/strikethrough styles
|
||||
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
let rasterizedGlyph: IRasterizedGlyph;
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the glyph
|
||||
if (chars && chars.length > 1) {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg);
|
||||
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
} else {
|
||||
rasterizedGlyph = this._atlas.getRasterizedGlyph(code, bg, fg);
|
||||
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
}
|
||||
|
||||
// Fill empty if no glyph was found
|
||||
if (!rasterizedGlyph) {
|
||||
fill(array, 0, i, i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
|
||||
if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
|
||||
$clippedPixels = $glyph.offset.x - $leftCellPadding;
|
||||
// a_origin
|
||||
array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
|
||||
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
|
||||
// a_size
|
||||
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
} else {
|
||||
// a_origin
|
||||
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
|
||||
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
|
||||
// a_size
|
||||
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
}
|
||||
|
||||
// a_origin
|
||||
array[i ] = -rasterizedGlyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[i + 1] = -rasterizedGlyph.offset.y + this._dimensions.scaledCharTop;
|
||||
// a_size
|
||||
array[i + 2] = rasterizedGlyph.size.x / this._dimensions.scaledCanvasWidth;
|
||||
array[i + 3] = rasterizedGlyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
// a_texcoord
|
||||
array[i + 4] = rasterizedGlyph.texturePositionClipSpace.x;
|
||||
array[i + 5] = rasterizedGlyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[i + 6] = rasterizedGlyph.sizeClipSpace.x;
|
||||
array[i + 7] = rasterizedGlyph.sizeClipSpace.y;
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
|
||||
public clear(force?: boolean): void {
|
||||
public clear(): void {
|
||||
const terminal = this._terminal;
|
||||
const newCount = terminal.cols * terminal.rows * INDICES_PER_CELL;
|
||||
|
||||
// Don't clear if not forced and the array length is correct
|
||||
if (!force && this._vertices.count === newCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear vertices
|
||||
this._vertices.count = newCount;
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.attributes = new Float32Array(newCount);
|
||||
} else {
|
||||
this._vertices.attributes.fill(0);
|
||||
}
|
||||
let i = 0;
|
||||
for (; i < this._vertices.attributesBuffers.length; i++) {
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
} else {
|
||||
this._vertices.attributesBuffers[i].fill(0);
|
||||
}
|
||||
}
|
||||
this._vertices.count = newCount;
|
||||
i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
this._vertices.attributes[i + 9] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 10] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
public handleResize(): void {
|
||||
const gl = this._gl;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
}
|
||||
|
||||
public render(renderModel: IRenderModel): void {
|
||||
if (!this._atlas) {
|
||||
return;
|
||||
@@ -278,31 +346,32 @@ export class GlyphRenderer {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the texture atlas if it's changed
|
||||
if (this._atlas.hasCanvasChanged) {
|
||||
this._atlas.hasCanvasChanged = false;
|
||||
gl.uniform1i(this._textureLocation, 0);
|
||||
gl.activeTexture(gl.TEXTURE0 + 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
// Bind the atlas page texture if they have changed
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Set uniforms
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: WebglCharAtlas): void {
|
||||
const gl = this._gl;
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
for (const glTexture of this._atlasTextures) {
|
||||
glTexture.version = -1;
|
||||
}
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
|
||||
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
this._atlasTextures[i].version = atlas.pages[i].version;
|
||||
}
|
||||
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
|
||||
import { Attributes, FgFlags } from 'common/buffer/Constants';
|
||||
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IThemeService } from 'browser/services/Services';
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
POSITION = 0,
|
||||
@@ -26,12 +30,11 @@ layout (location = ${VertexAttribLocations.COLOR}) in vec4 a_color;
|
||||
layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
|
||||
uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec4 v_color;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_position + (a_unitquad * a_size)) / u_resolution;
|
||||
vec2 zeroToOne = a_position + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_color = a_color;
|
||||
}`;
|
||||
@@ -57,11 +60,20 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
|
||||
|
||||
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
|
||||
|
||||
export class RectangleRenderer {
|
||||
// Work variables to avoid garbage collection
|
||||
let $rgba = 0;
|
||||
let $isDefault = false;
|
||||
let $x1 = 0;
|
||||
let $y1 = 0;
|
||||
let $r = 0;
|
||||
let $g = 0;
|
||||
let $b = 0;
|
||||
let $a = 0;
|
||||
|
||||
export class RectangleRenderer extends Disposable {
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _bgFloat!: Float32Array;
|
||||
@@ -73,16 +85,18 @@ export class RectangleRenderer {
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
private _dimensions: IRenderDimensions,
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
const gl = this._gl;
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
this.register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
this._resolutionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_resolution'));
|
||||
this._projectionLocation = throwIfFalsy(gl.getUniformLocation(this._program, 'u_projection'));
|
||||
|
||||
// Create and set the vertex array object
|
||||
@@ -92,20 +106,24 @@ export class RectangleRenderer {
|
||||
// Setup a_unitquad, this defines the 4 vertices of a rectangle
|
||||
const unitQuadVertices = new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]);
|
||||
const unitQuadVerticesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(unitQuadVerticesBuffer)));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, unitQuadVerticesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, unitQuadVertices, gl.STATIC_DRAW);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.UNIT_QUAD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertuces to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
|
||||
// triangle strip
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, unitQuadElementIndices, gl.STATIC_DRAW);
|
||||
|
||||
// Setup attributes
|
||||
this._attributesBuffer = throwIfFalsy(gl.createBuffer());
|
||||
this.register(toDisposable(() => gl.deleteBuffer(this._attributesBuffer)));
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.POSITION, 2, gl.FLOAT, false, BYTES_PER_RECTANGLE, 0);
|
||||
@@ -117,7 +135,11 @@ export class RectangleRenderer {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
|
||||
|
||||
this._updateCachedColors();
|
||||
this._updateCachedColors(_themeService.colors);
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._updateCachedColors(e);
|
||||
this._updateViewportRectangle();
|
||||
}));
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
@@ -128,25 +150,23 @@ export class RectangleRenderer {
|
||||
gl.bindVertexArray(this._vertexArrayObject);
|
||||
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Bind attributes buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
public handleResize(): void {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
this._updateCachedColors();
|
||||
this._updateViewportRectangle();
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
private _updateCachedColors(colors: ReadonlyColorSet): void {
|
||||
this._bgFloat = this._colorToFloat32Array(colors.background);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
@@ -156,8 +176,8 @@ export class RectangleRenderer {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
this._terminal.cols * this._dimensions.scaledCellWidth,
|
||||
this._terminal.rows * this._dimensions.scaledCellHeight,
|
||||
this._terminal.cols * this._dimensions.device.cell.width,
|
||||
this._terminal.rows * this._dimensions.device.cell.height,
|
||||
this._bgFloat
|
||||
);
|
||||
}
|
||||
@@ -166,22 +186,34 @@ export class RectangleRenderer {
|
||||
const terminal = this._terminal;
|
||||
const vertices = this._vertices;
|
||||
|
||||
// Declare variable ahead of time to avoid garbage collection
|
||||
let rectangleCount = 1;
|
||||
let y: number;
|
||||
let x: number;
|
||||
let currentStartX: number;
|
||||
let currentBg: number;
|
||||
let currentFg: number;
|
||||
let currentInverse: boolean;
|
||||
let modelIndex: number;
|
||||
let bg: number;
|
||||
let fg: number;
|
||||
let inverse: boolean;
|
||||
let offset: number;
|
||||
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
let currentStartX = -1;
|
||||
let currentBg = 0;
|
||||
let currentFg = 0;
|
||||
let currentInverse = false;
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
const bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
|
||||
const fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
|
||||
const inverse = !!(fg & FgFlags.INVERSE);
|
||||
for (y = 0; y < terminal.rows; y++) {
|
||||
currentStartX = -1;
|
||||
currentBg = 0;
|
||||
currentFg = 0;
|
||||
currentInverse = false;
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
modelIndex = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
bg = model.cells[modelIndex + RENDER_MODEL_BG_OFFSET];
|
||||
fg = model.cells[modelIndex + RENDER_MODEL_FG_OFFSET];
|
||||
inverse = !!(fg & FgFlags.INVERSE);
|
||||
if (bg !== currentBg || (fg !== currentFg && (currentInverse || inverse))) {
|
||||
// A rectangle needs to be drawn if going from non-default to another color
|
||||
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, x, y);
|
||||
}
|
||||
currentStartX = x;
|
||||
@@ -192,7 +224,7 @@ export class RectangleRenderer {
|
||||
}
|
||||
// Finish rectangle if it's still going
|
||||
if (currentBg !== 0 || (currentInverse && currentFg !== 0)) {
|
||||
const offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
offset = rectangleCount++ * INDICES_PER_RECTANGLE;
|
||||
this._updateRectangle(vertices, offset, currentFg, currentBg, currentStartX, terminal.cols, y);
|
||||
}
|
||||
}
|
||||
@@ -200,52 +232,54 @@ export class RectangleRenderer {
|
||||
}
|
||||
|
||||
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
|
||||
let rgba: number | undefined;
|
||||
$isDefault = false;
|
||||
if (fg & FgFlags.INVERSE) {
|
||||
switch (fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
$rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
$rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
rgba = this._colors.foreground.rgba;
|
||||
$rgba = this._themeService.colors.foreground.rgba;
|
||||
}
|
||||
} else {
|
||||
switch (bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
$rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
$rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
rgba = this._colors.background.rgba;
|
||||
$rgba = this._themeService.colors.background.rgba;
|
||||
$isDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
const x1 = startX * this._dimensions.scaledCellWidth;
|
||||
const y1 = y * this._dimensions.scaledCellHeight;
|
||||
const r = ((rgba >> 24) & 0xFF) / 255;
|
||||
const g = ((rgba >> 16) & 0xFF) / 255;
|
||||
const b = ((rgba >> 8 ) & 0xFF) / 255;
|
||||
$x1 = startX * this._dimensions.device.cell.width;
|
||||
$y1 = y * this._dimensions.device.cell.height;
|
||||
$r = (($rgba >> 24) & 0xFF) / 255;
|
||||
$g = (($rgba >> 16) & 0xFF) / 255;
|
||||
$b = (($rgba >> 8 ) & 0xFF) / 255;
|
||||
$a = 1;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, x1, y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, r, g, b, 1);
|
||||
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a);
|
||||
}
|
||||
|
||||
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset ] = x1 / this._dimensions.device.canvas.width;
|
||||
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
|
||||
array[offset + 2] = width / this._dimensions.device.canvas.width;
|
||||
array[offset + 3] = height / this._dimensions.device.canvas.height;
|
||||
array[offset + 4] = r;
|
||||
array[offset + 5] = g;
|
||||
array[offset + 6] = b;
|
||||
@@ -253,10 +287,10 @@ export class RectangleRenderer {
|
||||
}
|
||||
|
||||
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
|
||||
array[offset ] = x1;
|
||||
array[offset + 1] = y1;
|
||||
array[offset + 2] = width;
|
||||
array[offset + 3] = height;
|
||||
array[offset ] = x1 / this._dimensions.device.canvas.width;
|
||||
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
|
||||
array[offset + 2] = width / this._dimensions.device.canvas.width;
|
||||
array[offset + 3] = height / this._dimensions.device.canvas.height;
|
||||
array[offset + 4] = color[0];
|
||||
array[offset + 5] = color[1];
|
||||
array[offset + 6] = color[2];
|
||||
|
||||
@@ -3,12 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderModel, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { IRenderModel } from './Types';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 3;
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
export const RENDER_MODEL_BG_OFFSET = 1;
|
||||
export const RENDER_MODEL_FG_OFFSET = 2;
|
||||
export const RENDER_MODEL_EXT_OFFSET = 3;
|
||||
|
||||
export const COMBINED_CHAR_BIT_MASK = 0x80000000;
|
||||
|
||||
@@ -20,16 +22,7 @@ export class RenderModel implements IRenderModel {
|
||||
constructor() {
|
||||
this.cells = new Uint32Array(0);
|
||||
this.lineLengths = new Uint32Array(0);
|
||||
this.selection = {
|
||||
hasSelection: false,
|
||||
columnSelectMode: false,
|
||||
viewportStartRow: 0,
|
||||
viewportEndRow: 0,
|
||||
viewportCappedStartRow: 0,
|
||||
viewportCappedEndRow: 0,
|
||||
startCol: 0,
|
||||
endCol: 0
|
||||
};
|
||||
this.selection = createSelectionRenderModel();
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
@@ -41,17 +34,7 @@ export class RenderModel implements IRenderModel {
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
fill(this.cells, 0, 0);
|
||||
fill(this.lineLengths, 0, 0);
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selection.hasSelection = false;
|
||||
this.selection.viewportStartRow = 0;
|
||||
this.selection.viewportEndRow = 0;
|
||||
this.selection.viewportCappedStartRow = 0;
|
||||
this.selection.viewportCappedEndRow = 0;
|
||||
this.selection.startCol = 0;
|
||||
this.selection.endCol = 0;
|
||||
this.cells.fill(0, 0);
|
||||
this.lineLengths.fill(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-55
@@ -3,50 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export interface IRasterizedGlyphSet {
|
||||
[bg: number]: { [fg: number]: IRasterizedGlyph } | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRenderModel {
|
||||
cells: Uint32Array;
|
||||
@@ -54,17 +11,6 @@ export interface IRenderModel {
|
||||
selection: ISelectionRenderModel;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
hasSelection: boolean;
|
||||
columnSelectMode: boolean;
|
||||
viewportStartRow: number;
|
||||
viewportEndRow: number;
|
||||
viewportCappedStartRow: number;
|
||||
viewportCappedEndRow: number;
|
||||
startCol: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
createVertexArray(): IWebGLVertexArrayObject;
|
||||
|
||||
@@ -3,49 +3,78 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { getSafariVersion, isSafari } from 'common/Platform';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { ICharacterJoinerService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
export class WebglAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate WebglAddon before Terminal.open');
|
||||
) {
|
||||
if (isSafari && getSafariVersion() < 16) {
|
||||
throw new Error('Webgl2 is only supported on Safari 16 and above');
|
||||
}
|
||||
if (isSafari) {
|
||||
throw new Error('Webgl is not currently supported on Safari');
|
||||
}
|
||||
this._terminal = terminal;
|
||||
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
|
||||
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, decorationService, this._preserveDrawingBuffer);
|
||||
this._renderer.onContextLoss(() => this._onContextLoss.fire());
|
||||
renderService.setRenderer(this._renderer);
|
||||
super();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot dispose WebglAddon because it is activated');
|
||||
public activate(terminal: Terminal): void {
|
||||
const core = (terminal as any)._core as ITerminal;
|
||||
if (!terminal.element) {
|
||||
this.register(core.onWillOpen(() => this.activate(terminal)));
|
||||
return;
|
||||
}
|
||||
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this._renderer = undefined;
|
||||
|
||||
this._terminal = terminal;
|
||||
const coreService: ICoreService = core.coreService;
|
||||
const optionsService: IOptionsService = core.optionsService;
|
||||
|
||||
const unsafeCore = core as any;
|
||||
const renderService: IRenderService = unsafeCore._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
this._renderer = this.register(new WebglRenderer(
|
||||
terminal,
|
||||
characterJoinerService,
|
||||
charSizeService,
|
||||
coreBrowserService,
|
||||
coreService,
|
||||
decorationService,
|
||||
optionsService,
|
||||
themeService,
|
||||
this._preserveDrawingBuffer
|
||||
));
|
||||
this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
this.register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.handleResize(terminal.cols, terminal.rows);
|
||||
}));
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
@@ -53,6 +82,6 @@ export class WebglAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._renderer?.clearCharAtlas();
|
||||
this._renderer?.clearTextureAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,84 +3,90 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Attributes, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
||||
import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlas: WebglCharAtlas | undefined;
|
||||
private _charAtlasDisposable: IDisposable | undefined;
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _workCell: CellData = new CellData();
|
||||
private _workColors: { fg: number, bg: number } = { fg: 0, bg: 0 };
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
private _rectangleRenderer: RectangleRenderer;
|
||||
private _glyphRenderer: GlyphRenderer;
|
||||
private _rectangleRenderer?: RectangleRenderer;
|
||||
private _glyphRenderer?: GlyphRenderer;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
public readonly dimensions: IRenderDimensions;
|
||||
|
||||
private _core: ITerminal;
|
||||
private _isAttached: boolean;
|
||||
private _contextRestorationTimeout: number | undefined;
|
||||
|
||||
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
||||
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
private readonly _charSizeService: ICharSizeService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
coreService: ICoreService,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _themeService: IThemeService,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
|
||||
|
||||
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
|
||||
|
||||
this._core = (this._terminal as any)._core;
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core),
|
||||
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._core, this._onRequestRedraw)
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService),
|
||||
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, _optionsService, this._themeService)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
@@ -94,58 +100,67 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
throw new Error('WebGL2 not supported ' + this._gl);
|
||||
}
|
||||
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); }));
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => {
|
||||
console.log('webglcontextlost event received');
|
||||
// Prevent the default behavior in order to enable WebGL context restoration.
|
||||
e.preventDefault();
|
||||
// Wait a few seconds to see if the 'webglcontextrestored' event is fired.
|
||||
// If not, dispatch the onContextLoss notification to observers.
|
||||
this._contextRestorationTimeout = setTimeout(() => {
|
||||
this._contextRestorationTimeout = undefined;
|
||||
console.warn('webgl context not restored; firing onContextLoss');
|
||||
this._onContextLoss.fire(e);
|
||||
}, 3000 /* ms */);
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._canvas, 'webglcontextrestored', (e) => {
|
||||
console.warn('webglcontextrestored event received');
|
||||
clearTimeout(this._contextRestorationTimeout);
|
||||
this._contextRestorationTimeout = undefined;
|
||||
// The texture atlas and glyph renderer must be fully reinitialized
|
||||
// because their contents have been lost.
|
||||
removeTerminalFromCache(this._terminal);
|
||||
this._initializeWebGLState();
|
||||
this._requestRedrawViewport();
|
||||
}));
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
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._rectangleRenderer, this._glyphRenderer] = this._initializeWebGLState();
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.onCharSizeChanged();
|
||||
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
|
||||
|
||||
this._isAttached = document.body.contains(this._core.screenElement!);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._canvas.parentElement?.removeChild(this._canvas);
|
||||
super.dispose();
|
||||
this.register(toDisposable(() => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._canvas.parentElement?.removeChild(this._canvas);
|
||||
removeTerminalFromCache(this._terminal);
|
||||
}));
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._charAtlas?.cacheCanvas;
|
||||
return this._charAtlas?.pages[0].canvas;
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
// Clear layers and force a full render
|
||||
for (const l of this._renderLayers) {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
this._glyphRenderer.setColors();
|
||||
|
||||
private _handleColorChange(): void {
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
this._clearModel(true);
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== window.devicePixelRatio) {
|
||||
this._devicePixelRatio = window.devicePixelRatio;
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this.handleResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
@@ -157,94 +172,136 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
this._canvas.height = this.dimensions.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._canvas.width = this.dimensions.device.canvas.width;
|
||||
this._canvas.height = this.dimensions.device.canvas.height;
|
||||
this._canvas.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
|
||||
// Resize the screen
|
||||
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
|
||||
this._rectangleRenderer.onResize();
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
this._rectangleRenderer?.setDimensions(this.dimensions);
|
||||
this._rectangleRenderer?.handleResize();
|
||||
this._glyphRenderer?.setDimensions(this.dimensions);
|
||||
this._glyphRenderer?.handleResize();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._model.clear();
|
||||
// Force a full refresh. Resizing `_glyphRenderer` should clear it already,
|
||||
// so there is no need to clear it again here.
|
||||
this._clearModel(false);
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
public handleCharSizeChanged(): void {
|
||||
this.handleResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onBlur(this._terminal);
|
||||
l.handleBlur(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onFocus(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
this._updateSelectionModel(start, end, columnSelectMode);
|
||||
// Request a redraw for active/inactive selection background
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
public handleFocus(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onCursorMove(this._terminal);
|
||||
l.handleFocus(this._terminal);
|
||||
}
|
||||
// Request a redraw for active/inactive selection background
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
this._model.selection.update(this._terminal, start, end, columnSelectMode);
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public handleCursorMove(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.handleCursorMove(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onOptionsChanged(this._terminal);
|
||||
}
|
||||
private _handleOptionsChanged(): void {
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes members dependent on WebGL context state.
|
||||
*/
|
||||
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
|
||||
// Dispose any previous rectangle and glyph renderers before creating new ones.
|
||||
this._rectangleRenderer?.dispose();
|
||||
this._glyphRenderer?.dispose();
|
||||
|
||||
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService));
|
||||
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions));
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.handleCharSizeChanged();
|
||||
|
||||
return [this._rectangleRenderer, this._glyphRenderer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(): void {
|
||||
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
|
||||
if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) {
|
||||
// Mark as not attached so char atlas gets refreshed on next render
|
||||
this._isAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight);
|
||||
if (!('getRasterizedGlyph' in atlas)) {
|
||||
throw new Error('The webgl renderer only works with the webgl char atlas');
|
||||
const atlas = acquireTextureAtlas(
|
||||
this._terminal,
|
||||
this._optionsService.rawOptions,
|
||||
this._themeService.colors,
|
||||
this.dimensions.device.cell.width,
|
||||
this.dimensions.device.cell.height,
|
||||
this.dimensions.device.char.width,
|
||||
this.dimensions.device.char.height,
|
||||
this._coreBrowserService.dpr
|
||||
);
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
this._charAtlasDisposable = getDisposeArrayDisposable([
|
||||
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
||||
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
||||
]);
|
||||
}
|
||||
this._charAtlas = atlas as WebglCharAtlas;
|
||||
this._charAtlas = atlas;
|
||||
this._charAtlas.warmUp();
|
||||
this._glyphRenderer.setAtlas(this._charAtlas);
|
||||
this._glyphRenderer?.setAtlas(this._charAtlas);
|
||||
}
|
||||
|
||||
public clearCharAtlas(): void {
|
||||
this._charAtlas?.clearTexture();
|
||||
/**
|
||||
* Clear the model.
|
||||
* @param clearGlyphRenderer Whether to also clear the glyph renderer. This
|
||||
* should be true generally to make sure it is in the same state as the model.
|
||||
*/
|
||||
private _clearModel(clearGlyphRenderer: boolean): void {
|
||||
this._model.clear();
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
if (clearGlyphRenderer) {
|
||||
this._glyphRenderer?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._charAtlas?.clearTexture();
|
||||
this._clearModel(true);
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._model.clear();
|
||||
this._glyphRenderer.clear(true);
|
||||
this._clearModel(true);
|
||||
for (const l of this._renderLayers) {
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
@@ -260,7 +317,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
if (!this._isAttached) {
|
||||
if (document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) {
|
||||
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
this._isAttached = true;
|
||||
@@ -271,48 +328,75 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Update render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.onGridChanged(this._terminal, start, end);
|
||||
l.handleGridChanged(this._terminal, start, end);
|
||||
}
|
||||
|
||||
if (!this._glyphRenderer || !this._rectangleRenderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
// upon a model clear also refresh the full viewport model
|
||||
// (also triggered by an atlas page merge, part of #4480)
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._model.clear();
|
||||
this._updateSelectionModel(undefined, undefined);
|
||||
this._clearModel(true);
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
} else {
|
||||
// just update changed lines to draw
|
||||
this._updateModel(start, end);
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
this._updateModel(start, end);
|
||||
|
||||
// Render
|
||||
this._rectangleRenderer.render();
|
||||
this._glyphRenderer.render(this._model);
|
||||
this._rectangleRenderer?.render();
|
||||
this._glyphRenderer?.render(this._model);
|
||||
}
|
||||
|
||||
private _updateModel(start: number, end: number): void {
|
||||
const terminal = this._core;
|
||||
let cell: ICellData = this._workCell;
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row)!;
|
||||
// Declare variable ahead of time to avoid garbage collection
|
||||
let lastBg: number;
|
||||
let y: number;
|
||||
let row: number;
|
||||
let line: IBufferLine;
|
||||
let joinedRanges: [number, number][];
|
||||
let isJoined: boolean;
|
||||
let lastCharX: number;
|
||||
let range: [number, number];
|
||||
let chars: string;
|
||||
let code: number;
|
||||
let i: number;
|
||||
let x: number;
|
||||
let j: number;
|
||||
start = clamp(start, terminal.rows - 1, 0);
|
||||
end = clamp(end, terminal.rows - 1, 0);
|
||||
for (y = start; y <= end; y++) {
|
||||
row = y + terminal.buffer.ydisp;
|
||||
line = terminal.buffer.lines.get(row)!;
|
||||
this._model.lineLengths[y] = 0;
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
lastBg = this._cellColorResolver.result.bg;
|
||||
line.loadCell(x, cell);
|
||||
|
||||
if (x === 0) {
|
||||
lastBg = this._cellColorResolver.result.bg;
|
||||
}
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
isJoined = false;
|
||||
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()!;
|
||||
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
|
||||
// so we get the string and width representing it directly.
|
||||
cell = new JoinedCellData(
|
||||
cell,
|
||||
line!.translateToString(true, range[0], range[1]),
|
||||
@@ -323,12 +407,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
const chars = cell.getChars();
|
||||
let code = cell.getCode();
|
||||
const i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
chars = cell.getChars();
|
||||
code = cell.getCode();
|
||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._loadColorsForCell(x, row);
|
||||
this._cellColorResolver.resolve(cell, x, row);
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
@@ -336,8 +420,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg) {
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -348,10 +433,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, chars);
|
||||
this._glyphRenderer!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
@@ -359,216 +445,83 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// 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);
|
||||
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
|
||||
* necessary which is why the work cell object is not used.
|
||||
*/
|
||||
private _loadColorsForCell(x: number, y: number): void {
|
||||
this._workColors.bg = this._workCell.bg;
|
||||
this._workColors.fg = this._workCell.fg;
|
||||
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'bottom')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the selection color if needed
|
||||
if (this._isCellSelected(x, y)) {
|
||||
bgOverride = this._colors.selectionOpaque.rgba >> 8 & 0xFFFFFF;
|
||||
if (this._colors.selectionForeground) {
|
||||
fgOverride = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
for (const d of this._decorationService.getDecorationsAtCell(x, y, 'top')) {
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if (bgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
bgOverride = (this._workCell.bg & ~Attributes.RGB_MASK) | bgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
if (fgOverride !== undefined) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
fgOverride = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | fgOverride | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bgOverride or fgOverride was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this._workColors.fg & FgFlags.INVERSE) {
|
||||
if (bgOverride !== undefined && fgOverride === undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
fgOverride = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
if (bgOverride === undefined && fgOverride !== undefined) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
bgOverride = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this._workColors.bg = bgOverride ?? this._workColors.bg;
|
||||
this._workColors.fg = fgOverride ?? this._workColors.fg;
|
||||
}
|
||||
|
||||
private _isCellSelected(x: number, y: number): boolean {
|
||||
if (!this._model.selection.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= this._terminal.buffer.active.viewportY;
|
||||
if (this._model.selection.columnSelectMode) {
|
||||
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
||||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
this._model.selection.hasSelection = true;
|
||||
this._model.selection.columnSelectMode = columnSelectMode;
|
||||
this._model.selection.viewportStartRow = viewportStartRow;
|
||||
this._model.selection.viewportEndRow = viewportEndRow;
|
||||
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
this._rectangleRenderer!.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
// TODO: Acquire CharSizeService properly
|
||||
|
||||
// Perform a new measure if the CharMeasure dimensions are not yet available
|
||||
if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) {
|
||||
if (!this._charSizeService.width || !this._charSizeService.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be
|
||||
// drawn to an integer grid in order for the CharAtlas "stamps" to not be
|
||||
// blurry. When text is drawn to the grid not using the CharAtlas, it is
|
||||
// clipped to ensure there is no overlap with the next cell.
|
||||
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
|
||||
// in order for the char atlas glyphs to not be blurry.
|
||||
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
|
||||
|
||||
// NOTE: ceil fixes sometime, floor does others :s
|
||||
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
|
||||
// floating point number in order to ensure there is enough space to draw the character to the
|
||||
// cell.
|
||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
|
||||
|
||||
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
|
||||
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
|
||||
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
|
||||
// will always be larger than device char height.
|
||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case
|
||||
// devicePixelRatio is a floating point number in order to ensure there is
|
||||
// enough space to draw the character to the cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
|
||||
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
||||
|
||||
// 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
|
||||
// is a guarentee that the scaled line height will always be larger than
|
||||
// scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight!);
|
||||
// Calculate the device cell width, taking the letterSpacing into account.
|
||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
|
||||
// Calculate the y coordinate within a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
// Calculate the x offset with a cell that text should draw from in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing!);
|
||||
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
|
||||
// the canvas
|
||||
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
||||
|
||||
// Calculate the x coordinate with a cell that text should draw from in
|
||||
// order to draw in the center of a cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing! / 2);
|
||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// and not ceils as browsers often have floating point precision issues where
|
||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
||||
// large for the canvas element size.
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
||||
|
||||
// Recalculate the canvas dimensions; scaled* define the actual number of
|
||||
// pixel in the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
||||
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
|
||||
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
|
||||
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
|
||||
// size on the canvas can differ.
|
||||
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
|
||||
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
|
||||
}
|
||||
|
||||
// The the size of the canvas on the page. It's very important that this
|
||||
// rounds to nearest integer and not ceils as browsers often set
|
||||
// window.devicePixelRatio as something like 1.100000023841858, when it's
|
||||
// actually 1.1. Ceiling causes blurriness as the backing canvas image is 1
|
||||
// pixel too large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
||||
|
||||
// this.dimensions.scaledCanvasHeight = this.dimensions.canvasHeight * devicePixelRatio;
|
||||
// this.dimensions.scaledCanvasWidth = this.dimensions.canvasWidth * devicePixelRatio;
|
||||
|
||||
// Get the _actual_ dimensions of an individual cell. This needs to be
|
||||
// derived from the canvasWidth/Height calculated above which takes into
|
||||
// account window.devicePixelRatio. CharMeasure.width/height by itself is
|
||||
// insufficient when the page is not at 100% zoom level as CharMeasure is
|
||||
// measured in CSS pixels, but the actual char size on the canvas can
|
||||
// differ.
|
||||
// this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
// this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
|
||||
// This fixes 110% and 125%, not 150% or 175% though
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
if (this._canvas.width === width && this._canvas.height === height) {
|
||||
return;
|
||||
}
|
||||
// While the actual canvas size has changed, keep device canvas dimensions as the value before
|
||||
// the change as it's an exact multiple of the cell sizes.
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
private _requestRedrawViewport(): void {
|
||||
@@ -621,3 +574,7 @@ export class JoinedCellData extends AttributeData implements ICellData {
|
||||
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
|
||||
}
|
||||
}
|
||||
|
||||
function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
/**
|
||||
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
|
||||
* top to bottom) to clip space.
|
||||
@@ -50,9 +52,12 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
export class GLTexture {
|
||||
public texture: WebGLTexture;
|
||||
public version: number;
|
||||
|
||||
constructor(texture: WebGLTexture) {
|
||||
this.texture = texture;
|
||||
this.version = -1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
bold: boolean;
|
||||
dim: boolean;
|
||||
italic: boolean;
|
||||
}
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
customGlyphs: boolean;
|
||||
devicePixelRatio: number;
|
||||
letterSpacing: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
drawBoldTextInBrightColors: boolean;
|
||||
minimumContrastRatio: number;
|
||||
colors: IColorSet;
|
||||
}
|
||||
@@ -1,614 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types';
|
||||
import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { channels, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
/**
|
||||
* The amount of the texture to be filled before throwing it away and starting
|
||||
* again. Since the throw away and individual glyph draws don't cost too much,
|
||||
* this prevent juggling multiple textures in the GL context.
|
||||
*/
|
||||
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
*/
|
||||
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
|
||||
offset: { x: 0, y: 0 },
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: { x: 0, y: 0 },
|
||||
sizeClipSpace: { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
const TMP_CANVAS_GLYPH_PADDING = 2;
|
||||
|
||||
export class WebglCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
private _cacheMap: { [code: number]: IRasterizedGlyphSet } = {};
|
||||
private _cacheMapCombined: { [chars: string]: IRasterizedGlyphSet } = {};
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
public cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Since glyphs are expected to be around the same height, the packing
|
||||
// strategy used it to fill a row with glyphs while keeping track of the
|
||||
// tallest glyph in the row. Once the row is full a new row is started at
|
||||
// (0,lastRow+lastRowTallestGlyph).
|
||||
private _currentRowY: number = 0;
|
||||
private _currentRowX: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
|
||||
public hasCanvasChanged = false;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
private _workAttributeData: AttributeData = new AttributeData();
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
private _config: ICharAtlasConfig
|
||||
) {
|
||||
this.cacheCanvas = document.createElement('canvas');
|
||||
this.cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this.cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.cacheCanvas.parentElement) {
|
||||
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
|
||||
}
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
private _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126
|
||||
for (let i = 33; i < 126; i++) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR);
|
||||
this._cacheMap[i] = {
|
||||
[DEFAULT_COLOR]: {
|
||||
[DEFAULT_COLOR]: rasterizedGlyph
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
if (this._currentRowY > TEXTURE_CAPACITY) {
|
||||
this.clearTexture();
|
||||
this.warmUp();
|
||||
return true;
|
||||
}
|
||||
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 {
|
||||
let rasterizedGlyphSet = this._cacheMapCombined[chars];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMapCombined[chars] = rasterizedGlyphSet;
|
||||
}
|
||||
let rasterizedGlyph: IRasterizedGlyph | undefined;
|
||||
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (rasterizedGlyphSetBg) {
|
||||
rasterizedGlyph = rasterizedGlyphSetBg[fg];
|
||||
}
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(chars, bg, fg);
|
||||
if (!rasterizedGlyphSet[bg]) {
|
||||
rasterizedGlyphSet[bg] = {};
|
||||
}
|
||||
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
|
||||
}
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the glyphs texture coords, drawing the texture if it's not already
|
||||
*/
|
||||
public getRasterizedGlyph(code: number, bg: number, fg: number): IRasterizedGlyph {
|
||||
let rasterizedGlyphSet = this._cacheMap[code];
|
||||
if (!rasterizedGlyphSet) {
|
||||
rasterizedGlyphSet = {};
|
||||
this._cacheMap[code] = rasterizedGlyphSet;
|
||||
}
|
||||
let rasterizedGlyph: IRasterizedGlyph | undefined;
|
||||
const rasterizedGlyphSetBg = rasterizedGlyphSet[bg];
|
||||
if (rasterizedGlyphSetBg) {
|
||||
rasterizedGlyph = rasterizedGlyphSetBg[fg];
|
||||
}
|
||||
if (!rasterizedGlyph) {
|
||||
rasterizedGlyph = this._drawToCache(code, bg, fg);
|
||||
if (!rasterizedGlyphSet[bg]) {
|
||||
rasterizedGlyphSet[bg] = {};
|
||||
}
|
||||
rasterizedGlyphSet[bg]![fg] = rasterizedGlyph;
|
||||
}
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx >= this._config.colors.ansi.length) {
|
||||
throw new Error('No color found for idx ' + idx);
|
||||
}
|
||||
return this._config.colors.ansi[idx];
|
||||
}
|
||||
|
||||
private _getBackgroundColor(bgColorMode: number, bgColor: number, inverse: boolean): IColor {
|
||||
if (this._config.allowTransparency) {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return TRANSPARENT_COLOR;
|
||||
}
|
||||
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
return this._getColorFromAnsiIndex(bgColor);
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(bgColor);
|
||||
// TODO: This object creation is slow
|
||||
return {
|
||||
rgba: bgColor << 8,
|
||||
css: `#${toPaddedHex(arr[0])}${toPaddedHex(arr[1])}${toPaddedHex(arr[2])}`
|
||||
};
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
return this._config.colors.background;
|
||||
}
|
||||
}
|
||||
|
||||
private _getForegroundCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string {
|
||||
const minimumContrastCss = this._getMinimumContrastCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands);
|
||||
if (minimumContrastCss) {
|
||||
return minimumContrastCss;
|
||||
}
|
||||
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._getColorFromAnsiIndex(fgColor).css;
|
||||
case Attributes.CM_RGB:
|
||||
const arr = AttributeData.toColorRGB(fgColor);
|
||||
return channels.toCss(arr[0], arr[1], arr[2]);
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
const bg = this._config.colors.background.css;
|
||||
if (bg.length === 9) {
|
||||
// Remove bg alpha channel if present
|
||||
return bg.slice(0, 7);
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
return this._config.colors.foreground.css;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
return this._getColorFromAnsiIndex(bgColor).rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return bgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._config.colors.foreground.rgba;
|
||||
}
|
||||
return this._config.colors.background.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._config.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._getColorFromAnsiIndex(fgColor).rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return fgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._config.colors.background.rgba;
|
||||
}
|
||||
return this._config.colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _getMinimumContrastCss(bg: number, bgColorMode: number, bgColor: number, fg: number, fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean, excludeFromContrastRatioDemands: boolean): string | undefined {
|
||||
if (this._config.minimumContrastRatio === 1 || excludeFromContrastRatioDemands) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Try get from cache first
|
||||
const adjustedColor = this._config.colors.contrastCache.getCss(bg, fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgColorMode, bgColor, inverse);
|
||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, inverse, bold);
|
||||
const result = rgba.ensureContrastRatio(bgRgba, fgRgba, this._config.minimumContrastRatio);
|
||||
|
||||
if (!result) {
|
||||
this._config.colors.contrastCache.setCss(bg, fg, null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const css = channels.toCss(
|
||||
(result >> 24) & 0xFF,
|
||||
(result >> 16) & 0xFF,
|
||||
(result >> 8) & 0xFF
|
||||
);
|
||||
this._config.colors.contrastCache.setCss(bg, fg, css);
|
||||
|
||||
return css;
|
||||
}
|
||||
|
||||
private _drawToCache(code: number, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(chars: string, bg: number, fg: number): IRasterizedGlyph;
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
this._tmpCtx.save();
|
||||
|
||||
this._workAttributeData.fg = fg;
|
||||
this._workAttributeData.bg = bg;
|
||||
|
||||
const invisible = !!this._workAttributeData.isInvisible();
|
||||
if (invisible) {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const bold = !!this._workAttributeData.isBold();
|
||||
const inverse = !!this._workAttributeData.isInverse();
|
||||
const dim = !!this._workAttributeData.isDim();
|
||||
const italic = !!this._workAttributeData.isItalic();
|
||||
const underline = !!this._workAttributeData.isUnderline();
|
||||
const strikethrough = !!this._workAttributeData.isStrikethrough();
|
||||
let fgColor = this._workAttributeData.getFgColor();
|
||||
let fgColorMode = this._workAttributeData.getFgColorMode();
|
||||
let bgColor = this._workAttributeData.getBgColor();
|
||||
let bgColorMode = this._workAttributeData.getBgColorMode();
|
||||
if (inverse) {
|
||||
const temp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = temp;
|
||||
const temp2 = fgColorMode;
|
||||
fgColorMode = bgColorMode;
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
// draw the background
|
||||
const backgroundColor = this._getBackgroundColor(bgColorMode, bgColor, inverse);
|
||||
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
|
||||
// transparency in backgroundColor
|
||||
this._tmpCtx.globalCompositeOperation = 'copy';
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
const powerLineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0));
|
||||
this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0)));
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129)
|
||||
const padding = powerLineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING;
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
}
|
||||
|
||||
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible,
|
||||
// try for a maximum of 5 pixels.
|
||||
if (chars === '_' && !this._config.allowTransparency) {
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
|
||||
if (isBeyondCellBounds) {
|
||||
for (let offset = 1; offset <= 5; offset++) {
|
||||
this._tmpCtx.clearRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor);
|
||||
if (!isBeyondCellBounds) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw underline and strikethrough
|
||||
if (underline || strikethrough) {
|
||||
const lineWidth = Math.max(1, Math.floor(this._config.fontSize / 10));
|
||||
const yOffset = this._tmpCtx.lineWidth % 2 === 1 ? 0.5 : 0; // When the width is odd, draw at 0.5 position
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
this._tmpCtx.beginPath();
|
||||
if (underline) {
|
||||
this._tmpCtx.moveTo(padding, padding + this._config.scaledCharHeight - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + this._config.scaledCharHeight - yOffset);
|
||||
}
|
||||
if (strikethrough) {
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
}
|
||||
this._tmpCtx.stroke();
|
||||
this._tmpCtx.closePath();
|
||||
}
|
||||
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
const imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._tmpCanvas.width, this._tmpCanvas.height
|
||||
);
|
||||
|
||||
// TODO: Support transparency
|
||||
// let isEmpty = false;
|
||||
// if (!this._config.allowTransparency) {
|
||||
// isEmpty = clearColor(imageData, backgroundColor);
|
||||
// }
|
||||
|
||||
// Clear out the background color and determine if the glyph is empty.
|
||||
const isEmpty = clearColor(imageData, backgroundColor);
|
||||
|
||||
// Handle empty glyphs
|
||||
if (isEmpty) {
|
||||
return NULL_RASTERIZED_GLYPH;
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, powerLineGlyph, drawSuccess);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Check if there is enough room in the current row and go to next if needed
|
||||
if (this._currentRowX + rasterizedGlyph.size.x > TEXTURE_WIDTH) {
|
||||
this._currentRowX = 0;
|
||||
this._currentRowY += this._currentRowHeight;
|
||||
this._currentRowHeight = 0;
|
||||
}
|
||||
|
||||
// Record texture position
|
||||
rasterizedGlyph.texturePosition.x = this._currentRowX;
|
||||
rasterizedGlyph.texturePosition.y = this._currentRowY;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = this._currentRowX / TEXTURE_WIDTH;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = this._currentRowY / TEXTURE_HEIGHT;
|
||||
|
||||
// Update atlas current row
|
||||
this._currentRowHeight = Math.max(this._currentRowHeight, rasterizedGlyph.size.y);
|
||||
this._currentRowX += rasterizedGlyph.size.x;
|
||||
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an ImageData object, find the bounding box of the non-transparent
|
||||
* portion of the texture and return an IRasterizedGlyph with these
|
||||
* dimensions.
|
||||
* @param imageData The image data to read.
|
||||
* @param boundingBox An IBoundingBox to put the clipped bounding box values.
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCharWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.top = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.left = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.right = width;
|
||||
found = false;
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.right = x;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
boundingBox.bottom = height;
|
||||
found = false;
|
||||
for (let y = height - 1; y >= 0; y--) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
boundingBox.bottom = y;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: {
|
||||
x: boundingBox.right - boundingBox.left + 1,
|
||||
y: boundingBox.bottom - boundingBox.top + 1
|
||||
},
|
||||
sizeClipSpace: {
|
||||
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? Math.floor(this._config.letterSpacing / 2) : 0),
|
||||
y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) + (customGlyph ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
|
||||
const width = boundingBox.right - boundingBox.left + 1;
|
||||
const height = boundingBox.bottom - boundingBox.top + 1;
|
||||
const clippedData = new Uint8ClampedArray(width * height * 4);
|
||||
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
|
||||
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
|
||||
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
|
||||
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
|
||||
clippedData[newOffset] = imageData.data[oldOffset];
|
||||
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
|
||||
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
|
||||
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
|
||||
}
|
||||
}
|
||||
return new ImageData(clippedData, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a partiicular rgb color in an ImageData completely transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, color: IColor): boolean {
|
||||
let isEmpty = true;
|
||||
const r = color.rgba >>> 24;
|
||||
const g = color.rgba >>> 16 & 0xFF;
|
||||
const b = color.rgba >>> 8 & 0xFF;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (imageData.data[offset] === r &&
|
||||
imageData.data[offset + 1] === g &&
|
||||
imageData.data[offset + 2] === b) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
return isEmpty;
|
||||
}
|
||||
|
||||
function toPaddedHex(c: number): string {
|
||||
const s = c.toString(16);
|
||||
return s.length < 2 ? '0' + s : s;
|
||||
}
|
||||
@@ -4,46 +4,53 @@
|
||||
*/
|
||||
|
||||
import { IRenderLayer } from './Types';
|
||||
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
|
||||
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/atlas/Constants';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
private _scaledCellHeight: number = 0;
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
private _deviceCharWidth: number = 0;
|
||||
private _deviceCharHeight: number = 0;
|
||||
private _deviceCellWidth: number = 0;
|
||||
private _deviceCellHeight: number = 0;
|
||||
private _deviceCharLeft: number = 0;
|
||||
private _deviceCharTop: number = 0;
|
||||
|
||||
protected _charAtlas: WebglCharAtlas | undefined;
|
||||
protected _charAtlas: ITextureAtlas | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet
|
||||
protected readonly _coreBrowserService: ICoreBrowserService,
|
||||
protected readonly _optionsService: IOptionsService,
|
||||
protected readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.dispose();
|
||||
}
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._refreshCharAtlas(terminal, e);
|
||||
this.reset(terminal);
|
||||
}));
|
||||
this.register(toDisposable(() => {
|
||||
this._canvas.remove();
|
||||
this._charAtlas?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
@@ -54,16 +61,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {}
|
||||
public onBlur(terminal: Terminal): void {}
|
||||
public onFocus(terminal: Terminal): void {}
|
||||
public onCursorMove(terminal: Terminal): void {}
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
public setColors(terminal: Terminal, colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
}
|
||||
public handleBlur(terminal: Terminal): void {}
|
||||
public handleFocus(terminal: Terminal): void {}
|
||||
public handleCursorMove(terminal: Terminal): void {}
|
||||
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||
public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
@@ -80,8 +82,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
this._refreshCharAtlas(terminal, this._themeService.colors);
|
||||
this.handleGridChanged(terminal, 0, terminal.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,32 +91,32 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
|
||||
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
this._deviceCellWidth = dim.device.cell.width;
|
||||
this._deviceCellHeight = dim.device.cell.height;
|
||||
this._deviceCharWidth = dim.device.char.width;
|
||||
this._deviceCharHeight = dim.device.char.height;
|
||||
this._deviceCharLeft = dim.device.char.left;
|
||||
this._deviceCharTop = dim.device.char.top;
|
||||
this._canvas.width = dim.device.canvas.width;
|
||||
this._canvas.height = dim.device.canvas.height;
|
||||
this._canvas.style.width = `${dim.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${dim.css.canvas.height}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this._refreshCharAtlas(terminal, this._themeService.colors);
|
||||
}
|
||||
|
||||
public abstract reset(terminal: Terminal): void;
|
||||
@@ -128,10 +130,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,10 +144,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - window.devicePixelRatio - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
window.devicePixelRatio);
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,10 +158,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
window.devicePixelRatio * width,
|
||||
this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
this._coreBrowserService.dpr * width,
|
||||
this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -169,12 +171,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.lineWidth = window.devicePixelRatio;
|
||||
this._ctx.lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCellWidth + window.devicePixelRatio / 2,
|
||||
y * this._scaledCellHeight + (window.devicePixelRatio / 2),
|
||||
width * this._scaledCellWidth - window.devicePixelRatio,
|
||||
(height * this._scaledCellHeight) - window.devicePixelRatio);
|
||||
x * this._deviceCellWidth + this._coreBrowserService.dpr / 2,
|
||||
y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2),
|
||||
width * this._deviceCellWidth - this._coreBrowserService.dpr,
|
||||
(height * this._deviceCellHeight) - this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +186,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
@@ -199,17 +201,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -221,30 +223,30 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
this._clipRow(terminal, y);
|
||||
this._clipCell(x, y, cell.getWidth());
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
x * this._deviceCellWidth + this._deviceCharLeft,
|
||||
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param terminal The terminal.
|
||||
* Clips a cell to ensure no pixels will be drawn outside of it.
|
||||
* @param x The column to clip.
|
||||
* @param y The row to clip.
|
||||
* @param width The number of columns to clip.
|
||||
*/
|
||||
private _clipRow(terminal: Terminal, y: number): void {
|
||||
private _clipCell(x: number, y: number, width: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
terminal.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
this._deviceCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
@@ -257,7 +259,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize! * this._coreBrowserService.dpr}px ${terminal.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@ import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { isFirefox } from 'common/Platform';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -34,11 +37,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
private readonly _terminal: ITerminal,
|
||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>
|
||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
private readonly _coreService: ICoreService,
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors);
|
||||
super(terminal, container, 'cursor', zIndex, true, coreBrowserService, optionsService, themeService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -51,7 +56,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
this.onOptionsChanged(terminal);
|
||||
this._handleOptionsChanged(terminal);
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal)));
|
||||
this.register(toDisposable(() => {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -69,25 +79,25 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
public reset(terminal: Terminal): void {
|
||||
this._clearCursor();
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
this.onOptionsChanged(terminal);
|
||||
this._handleOptionsChanged(terminal);
|
||||
}
|
||||
|
||||
public onBlur(terminal: Terminal): void {
|
||||
public handleBlur(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onFocus(terminal: Terminal): void {
|
||||
public handleFocus(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.resume(terminal);
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {
|
||||
private _handleOptionsChanged(terminal: Terminal): void {
|
||||
if (terminal.options.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(terminal, () => {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(() => {
|
||||
this._render(terminal, true);
|
||||
});
|
||||
}, this._coreBrowserService);
|
||||
}
|
||||
} else {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
@@ -98,11 +108,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onCursorMove(terminal: Terminal): void {
|
||||
public handleCursorMove(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(terminal, false);
|
||||
} else {
|
||||
@@ -112,8 +122,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _render(terminal: Terminal, triggeredByAnimationFrame: boolean): void {
|
||||
// Don't draw the cursor if it's hidden
|
||||
// TODO: Need to expose API for this
|
||||
if (!this._terminal.coreService.isCursorInitialized || this._terminal.coreService.isCursorHidden) {
|
||||
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
@@ -136,16 +145,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isTerminalFocused(terminal)) {
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
const cursorStyle = terminal.options.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
} else {
|
||||
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
}
|
||||
this._renderBlurCursor(terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
@@ -165,7 +170,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
// The cursor is already in the correct spot, don't redraw
|
||||
if (this._state.x === cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === isTerminalFocused(terminal) &&
|
||||
this._state.isFocused === this._coreBrowserService.isFocused &&
|
||||
this._state.style === terminal.options.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
@@ -186,8 +191,9 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
// Avoid potential rounding errors when device pixel ratio is less than 1
|
||||
if (window.devicePixelRatio < 1) {
|
||||
// Avoid potential rounding errors when browser is Firefox (#4487) or device pixel ratio is
|
||||
// less than 1
|
||||
if (isFirefox || this._coreBrowserService.dpr < 1) {
|
||||
this._clearAll();
|
||||
} else {
|
||||
this._clearCells(this._state.x, this._state.y, this._state.width, 1);
|
||||
@@ -204,30 +210,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth!);
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(terminal, cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
@@ -248,11 +254,11 @@ class CursorBlinkStateManager {
|
||||
private _animationTimeRestarted: number | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
private _renderCallback: () => void
|
||||
private _renderCallback: () => void,
|
||||
private _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
this.isCursorVisible = true;
|
||||
if (isTerminalFocused(terminal)) {
|
||||
if (this._coreBrowserService.isFocused) {
|
||||
this._restartInterval();
|
||||
}
|
||||
}
|
||||
@@ -261,15 +267,15 @@ class CursorBlinkStateManager {
|
||||
|
||||
public dispose(): void {
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
@@ -283,7 +289,7 @@ class CursorBlinkStateManager {
|
||||
// Force a cursor render to ensure it's visible and in the correct position
|
||||
this.isCursorVisible = true;
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
@@ -293,7 +299,7 @@ class CursorBlinkStateManager {
|
||||
private _restartInterval(timeToStart: number = BLINK_INTERVAL): void {
|
||||
// Clear any existing interval
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
|
||||
@@ -301,7 +307,7 @@ class CursorBlinkStateManager {
|
||||
// the regular interval is setup in order to support restarting the blink
|
||||
// animation in a lightweight way (without thrashing clearInterval and
|
||||
// setInterval).
|
||||
this._blinkStartTimeout = window.setTimeout(() => {
|
||||
this._blinkStartTimeout = this._coreBrowserService.window.setTimeout(() => {
|
||||
// Check if another animation restart was requested while this was being
|
||||
// started
|
||||
if (this._animationTimeRestarted) {
|
||||
@@ -315,13 +321,13 @@ class CursorBlinkStateManager {
|
||||
|
||||
// Hide the cursor
|
||||
this.isCursorVisible = false;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
|
||||
// Setup the blink interval
|
||||
this._blinkInterval = window.setInterval(() => {
|
||||
this._blinkInterval = this._coreBrowserService.window.setInterval(() => {
|
||||
// Adjust the animation time if it was restarted
|
||||
if (this._animationTimeRestarted) {
|
||||
// calc time diff
|
||||
@@ -334,7 +340,7 @@ class CursorBlinkStateManager {
|
||||
|
||||
// Invert visibility and render
|
||||
this.isCursorVisible = !this.isCursorVisible;
|
||||
this._animationFrame = window.requestAnimationFrame(() => {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
|
||||
this._renderCallback();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
@@ -345,15 +351,15 @@ class CursorBlinkStateManager {
|
||||
public pause(): void {
|
||||
this.isCursorVisible = true;
|
||||
if (this._blinkInterval) {
|
||||
window.clearInterval(this._blinkInterval);
|
||||
this._coreBrowserService.window.clearInterval(this._blinkInterval);
|
||||
this._blinkInterval = undefined;
|
||||
}
|
||||
if (this._blinkStartTimeout) {
|
||||
window.clearTimeout(this._blinkStartTimeout);
|
||||
this._coreBrowserService.window.clearTimeout(this._blinkStartTimeout);
|
||||
this._blinkStartTimeout = undefined;
|
||||
}
|
||||
if (this._animationFrame) {
|
||||
window.cancelAnimationFrame(this._animationFrame);
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
}
|
||||
@@ -367,7 +373,3 @@ class CursorBlinkStateManager {
|
||||
this.restartBlinkAnimation(terminal);
|
||||
}
|
||||
}
|
||||
|
||||
function isTerminalFocused(terminal: Terminal): boolean {
|
||||
return document.activeElement === terminal.textarea && document.hasFocus();
|
||||
}
|
||||
|
||||
@@ -3,23 +3,31 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants';
|
||||
import { is256Color } from '../atlas/CharAtlasUtils';
|
||||
import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) {
|
||||
super(container, 'link', zIndex, true, colors);
|
||||
terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
terminal: Terminal,
|
||||
linkifier2: ILinkifier2,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService);
|
||||
|
||||
terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
|
||||
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -44,14 +52,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
|
||||
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
@@ -68,7 +76,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,45 +4,34 @@
|
||||
*/
|
||||
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(terminal: Terminal): void;
|
||||
handleBlur(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
* Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(terminal: Terminal): void;
|
||||
handleFocus(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(terminal: Terminal, colorSet: IColorSet): void;
|
||||
handleCursorMove(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
|
||||
handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
"es6"
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../out",
|
||||
|
||||
@@ -252,7 +252,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[38;5;${16 + y * 16 + x}m█\x1b[0m`;
|
||||
data += `\\x1b[38;5;${16 + y * 16 + x}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -312,7 +312,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\x1b[0m`;
|
||||
data += `\\x1b[7;48;5;${16 + y * 16 + x}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -348,7 +348,56 @@ describe('WebGL Renderer Integration Tests', 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 += `\\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.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
await pollFor(page, () => getCellColor(x + 1, y + 1), [r, g, b, 255]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
itWebgl('foreground 16-255 dim', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[2;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++) {
|
||||
const cssColor = COLORS_16_TO_255[y * 16 + x];
|
||||
const r = parseInt(cssColor.slice(1, 3), 16);
|
||||
const g = parseInt(cssColor.slice(3, 5), 16);
|
||||
const b = parseInt(cssColor.slice(5, 7), 16);
|
||||
// It's difficult to assert the exact color due to rounding, just ensure the color differs
|
||||
// to the regular color
|
||||
await pollFor(page, async () => {
|
||||
const c = await getCellColor(x + 1, y + 1);
|
||||
return (
|
||||
(c[0] === 0 || c[0] !== r) &&
|
||||
(c[1] === 0 || c[1] !== g) &&
|
||||
(c[2] === 0 || c[2] !== b)
|
||||
);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
itWebgl('background 16-255 dim', async () => {
|
||||
let data = '';
|
||||
for (let y = 0; y < 240 / 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
data += `\\x1b[2;48;5;${16 + y * 16 + x}m \\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -369,7 +418,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[38;2;${i};0;0m█\x1b[0m`;
|
||||
data += `\\x1b[38;2;${i};0;0m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -405,7 +454,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[38;2;0;${i};0m█\x1b[0m`;
|
||||
data += `\\x1b[38;2;0;${i};0m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -441,7 +490,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[38;2;0;0;${i}m█\x1b[0m`;
|
||||
data += `\\x1b[38;2;0;0;${i}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -477,7 +526,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[38;2;${i};${i};${i}m█\x1b[0m`;
|
||||
data += `\\x1b[38;2;${i};${i};${i}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -567,7 +616,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[7;48;2;0;${i};0m█\x1b[0m`;
|
||||
data += `\\x1b[7;48;2;0;${i};0m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -603,7 +652,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[7;48;2;0;0;${i}m█\x1b[0m`;
|
||||
data += `\\x1b[7;48;2;0;0;${i}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -639,7 +688,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
for (let y = 0; y < 16; y++) {
|
||||
for (let x = 0; x < 16; x++) {
|
||||
const i = y * 16 + x;
|
||||
data += `\\x1b[7;48;2;${i};${i};${i}m█\x1b[0m`;
|
||||
data += `\\x1b[7;48;2;${i};${i};${i}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -674,7 +723,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
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 += `\\x1b[8;48;2;${i};${i};${i}m█\\x1b[0m`;
|
||||
}
|
||||
data += '\\r\\n';
|
||||
}
|
||||
@@ -832,7 +881,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selection', async () => {
|
||||
describe('selectionBackground', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser());
|
||||
after(async () => browser.close());
|
||||
@@ -843,7 +892,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
const theme: ITheme = {
|
||||
foreground: '#FF0000',
|
||||
background: '#00FF00',
|
||||
selection: '#0000FF'
|
||||
selectionBackground: '#0000FF'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
await writeSync(page, ` █\\x1b[7m█\\x1b[0m`);
|
||||
@@ -860,7 +909,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
|
||||
describe('allowTransparency', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true }));
|
||||
before(async () => setupBrowser({ allowTransparency: true }));
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
@@ -870,7 +919,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
background: '#ff000080'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
const data = `\\x1b[7m█\\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
// Inverse background should be opaque
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
@@ -879,7 +928,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
|
||||
describe('selectionForeground', () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
before(async () => setupBrowser());
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
@@ -889,7 +938,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
selectionForeground: '#ff0000'
|
||||
};
|
||||
await page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
|
||||
const data = `\\x1b[7m█\x1b[0m`;
|
||||
const data = `\\x1b[7m█\\x1b[0m`;
|
||||
await writeSync(page, data);
|
||||
await page.evaluate(`window.term.selectAll()`);
|
||||
await pollFor(page, () => getCellColor(1, 1), [255, 0, 0, 255]);
|
||||
@@ -898,7 +947,7 @@ describe('WebGL Renderer Integration Tests', async () => {
|
||||
|
||||
describe('decoration color overrides', async () => {
|
||||
if (areTestsEnabled) {
|
||||
before(async () => setupBrowser({ rendererType: 'dom' }));
|
||||
before(async () => setupBrowser());
|
||||
after(async () => browser.close());
|
||||
beforeEach(async () => page.evaluate(`window.term.reset()`));
|
||||
}
|
||||
@@ -992,8 +1041,8 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
window.result = new Uint8Array(4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor((${col - 0.5}) * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight),
|
||||
Math.floor((${col - 0.5}) * window.d.device.cell.width),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height),
|
||||
1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
@@ -1003,18 +1052,18 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
async function getCellPixels(col: number, row: number): Promise<number[]> {
|
||||
await page.evaluate(`
|
||||
window.gl = window.term._core._renderService._renderer._gl;
|
||||
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
|
||||
window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor(${col - 1} * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
|
||||
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
Math.floor(${col - 1} * window.d.device.cell.width),
|
||||
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height),
|
||||
window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
}
|
||||
|
||||
async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise<void> {
|
||||
async function setupBrowser(options: ITerminalOptions = {}): Promise<void> {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": [
|
||||
"es6",
|
||||
],
|
||||
|
||||
+15
-5
@@ -12,6 +12,21 @@ declare module 'xterm-addon-webgl' {
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
public textureAtlas?: HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* An event that is fired when the renderer loses its canvas context.
|
||||
*/
|
||||
public readonly onContextLoss: IEvent<void>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the texture atlas of the renderer changes.
|
||||
*/
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor(preserveDrawingBuffer?: boolean);
|
||||
|
||||
/**
|
||||
@@ -29,10 +44,5 @@ declare module 'xterm-addon-webgl' {
|
||||
* Clears the terminal's texture atlas and triggers a redraw.
|
||||
*/
|
||||
public clearTextureAtlas(): void;
|
||||
|
||||
/**
|
||||
* Fired when the WebglRenderer loses context
|
||||
*/
|
||||
public get onContextLoss(): IEvent<void>;
|
||||
}
|
||||
}
|
||||
|
||||
+19
-16
@@ -1,16 +1,19 @@
|
||||
# Node.js
|
||||
# Build a general Node.js application with npm.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
|
||||
pr:
|
||||
branches:
|
||||
include: ["main"]
|
||||
|
||||
trigger:
|
||||
branches:
|
||||
include: ["main"]
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: 'ubuntu-20.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -39,11 +42,11 @@ jobs:
|
||||
|
||||
- job: macOS
|
||||
pool:
|
||||
vmImage: 'macOS-10.15'
|
||||
vmImage: 'macOS-11'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -63,7 +66,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: CacheBeta@1
|
||||
inputs:
|
||||
@@ -79,7 +82,7 @@ jobs:
|
||||
|
||||
- job: Linux_IntegrationTests
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: 'ubuntu-20.04'
|
||||
steps:
|
||||
- script: |
|
||||
# source: https://github.com/microsoft/playwright/issues/1041
|
||||
@@ -92,7 +95,7 @@ jobs:
|
||||
displayName: Install required packages
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
@@ -108,11 +111,11 @@ jobs:
|
||||
# Integration tests are too flaky on macOS https://github.com/xtermjs/xterm.js/issues/3590
|
||||
# - job: macOS_IntegrationTests
|
||||
# pool:
|
||||
# vmImage: 'macOS-10.15'
|
||||
# vmImage: 'macOS-11'
|
||||
# steps:
|
||||
# - task: NodeTool@0
|
||||
# inputs:
|
||||
# versionSpec: '14.x'
|
||||
# versionSpec: '18.x'
|
||||
# displayName: 'Install Node.js'
|
||||
# - script: yarn --frozen-lockfile
|
||||
# displayName: 'Install dependencies and build'
|
||||
@@ -129,7 +132,7 @@ jobs:
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: yarn --frozen-lockfile
|
||||
displayName: 'Install dependencies and build'
|
||||
@@ -148,11 +151,11 @@ jobs:
|
||||
- Windows_IntegrationTests
|
||||
condition: and(succeeded(), or(eq(variables['Build.SourceBranch'], 'refs/heads/master'), eq(variables['FORCE_RELEASE'], 'true')))
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: 'ubuntu-20.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '14.x'
|
||||
versionSpec: '18.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: YarnInstaller@3
|
||||
inputs:
|
||||
|
||||
@@ -82,14 +82,15 @@ This document lists xterm.js' support of terminal sequences. The sequences are g
|
||||
- OSC - Operating System Command: sequence starting with \`ESC ]\` (7bit) or OSC (\`\\x9D\`, 8bit)
|
||||
|
||||
Application Program Command (APC), Privacy Message (PM) and Start of String (SOS) are recognized but not supported,
|
||||
any sequence of these types will be ignored. They are also not hookable by the API.
|
||||
any sequence of these types will be silently ignored. They are also not hookable by the API.
|
||||
|
||||
Note that the list only contains sequences implemented in xterm.js' core codebase. Missing sequences are either
|
||||
not supported or unstable/experimental. Furthermore addons or integrations can provide additional custom sequences.
|
||||
Note that the list only marks sequences implemented in xterm.js' core codebase as supported. Missing sequences are either
|
||||
not supported or unstable/experimental. Furthermore addons or integrations can provide additional custom sequences
|
||||
(denoted as "External" where known).
|
||||
|
||||
To denote the sequences the tables use the same abbreviations as xterm does:
|
||||
- \`Ps\`: A single (usually optional) numeric parameter, composed of one or more decimal digits.
|
||||
- \`Pm\`: A multiple numeric parameter composed of any number of single numeric parameters, separated by ; character(s),
|
||||
- \`Pm\`: Multiple numeric parameters composed of any number of single numeric parameters, separated by ; character(s),
|
||||
e.g. \` Ps ; Ps ; ... \`.
|
||||
- \`Pt\`: A text parameter composed of printable characters. Note that for most commands with \`Pt\` only
|
||||
ASCII printables are specified to work. Additionally the parser will let pass any codepoint greater than C1 as printable.
|
||||
@@ -334,7 +335,9 @@ const MACRO = [
|
||||
// #P[reason] - partial support with a reason as title
|
||||
[/#P\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Partial</span>`],
|
||||
// #B[reason] - supported but broken in a certain way, reason in title
|
||||
[/#B\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Broken</span>`]
|
||||
[/#B\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">Broken</span>`],
|
||||
// #E[notes] - support via external resource, eg. addon
|
||||
[/#E\[(.*?)\]/g, (s, p1) => `<span title="${p1}" style="text-decoration: underline">External</span>`]
|
||||
];
|
||||
|
||||
function applyMacros(s) {
|
||||
|
||||
@@ -28,6 +28,7 @@ if (changedFiles.some(e => e.search(/^addons\//) === -1)) {
|
||||
// Publish addons if any files were changed inside of the addon
|
||||
const addonPackageDirs = [
|
||||
path.resolve(__dirname, '../addons/xterm-addon-attach'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-canvas'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-fit'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-ligatures'),
|
||||
path.resolve(__dirname, '../addons/xterm-addon-search'),
|
||||
|
||||
+22
-5
@@ -4,7 +4,7 @@
|
||||
*
|
||||
* Script to test different mouse modes in terminal emulators.
|
||||
* Tests for protocols DECSET 9, 1000, 1002, 1003 with different
|
||||
* report encodings (default, UTF8, SGR, URXVT).
|
||||
* report encodings (default, UTF8, SGR, URXVT, SGR-pixels).
|
||||
*
|
||||
* VT200 Highlight mode (DECSET 1001) is not implemented.
|
||||
*
|
||||
@@ -152,13 +152,27 @@ const ENC = {
|
||||
],
|
||||
'URXVT': [
|
||||
'\x1b[?1015h',
|
||||
// format: CSI <button + 32> ; Prow ; Pcol M
|
||||
// format: CSI <button + 32> ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(2, -1);
|
||||
const [button, col, row] = sReport.split(';');
|
||||
return {state: evalButtonCode(button - 32), row, col};
|
||||
}
|
||||
],
|
||||
'SGR_PIXELS' : [
|
||||
'\x1b[?1016h',
|
||||
// format: CSI < Pbutton ; Prow ; Pcol M
|
||||
report => {
|
||||
// strip off introducer + M
|
||||
const sReport = report.toString().slice(3, -1);
|
||||
const [buttonCode, col, row] = sReport.split(';');
|
||||
const state = evalButtonCode(buttonCode);
|
||||
if (report[report.length - 1] === 'm'.charCodeAt(0)) {
|
||||
state.action = 'release';
|
||||
}
|
||||
return {state, row, col};
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -203,9 +217,12 @@ function activate() {
|
||||
|
||||
function applyReportData(data) {
|
||||
let {state, row, col} = ENC[Object.keys(ENC)[activeEnc]][1](data);
|
||||
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
|
||||
// apply to cursor position
|
||||
process.stdout.write(`\x1b[${row};${col}H`);
|
||||
if (Object.keys(ENC)[activeEnc] !== 'SGR_PIXELS') {
|
||||
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'row:', row, 'col:', col);
|
||||
process.stdout.write(`\x1b[${row};${col}H`);
|
||||
} else {
|
||||
console.log('\x1b[2KButton:', state.button, 'Action:', state.action, 'Modifier:', state.modifier, 'x:', row, 'y:', col);
|
||||
}
|
||||
}
|
||||
|
||||
printMenu();
|
||||
|
||||
+23
-4
@@ -149,6 +149,7 @@
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
@@ -160,13 +161,27 @@
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
opacity: 0.5;
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline {
|
||||
text-decoration: underline;
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
@@ -176,8 +191,12 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 7;
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
|
||||
+655
-116
File diff suppressed because it is too large
Load Diff
+18
-2
@@ -10,8 +10,6 @@
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||
<link rel="stylesheet" href="/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
|
||||
@@ -25,6 +23,7 @@
|
||||
<button id= "addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
|
||||
<button id= "stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
|
||||
<button id= "testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
|
||||
<button id= "vtbutton" class="tabLinks" onclick="openSection(event, 'vt')">VT</button>
|
||||
</div>
|
||||
<div id="options" class="tabContent">
|
||||
<h3>Options</h3>
|
||||
@@ -70,22 +69,39 @@
|
||||
<dl>
|
||||
<dt>Lifecycle</dt>
|
||||
<dd><button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button></dd>
|
||||
<dd><button id="create-new-window" title="This is used to test rendering in other windows">Create terminal in new window</button></dd>
|
||||
|
||||
<dt>Performance</dt>
|
||||
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
|
||||
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
|
||||
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
|
||||
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
|
||||
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
|
||||
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
|
||||
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
|
||||
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
|
||||
|
||||
<dt>Weblinks Addon</dt>
|
||||
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vt" class="tabContent">
|
||||
<h3>VT</h3>
|
||||
<div id="vt-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" id="texture-atlas-zoom"/>
|
||||
<label for="texture-atlas-zoom">Zoom texture atlas</label>
|
||||
<div id="texture-atlas"></div>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
<script>
|
||||
var tab = localStorage.getItem("tab");
|
||||
|
||||
+47
-23
@@ -17,22 +17,23 @@ function startServer() {
|
||||
expressWs(app);
|
||||
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
unsentOutput = {},
|
||||
temporaryDisposable = {};
|
||||
|
||||
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
|
||||
app.get('/logo.png', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/logo.png', (req, res) => {
|
||||
res.sendFile(__dirname + '/logo.png');
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/', (req, res) => {
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
app.get('/test', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/test', (req, res) => {
|
||||
res.sendFile(__dirname + '/test.html');
|
||||
});
|
||||
|
||||
app.get('/style.css', (req, res) => { // lgtm [js/missing-rate-limiting]
|
||||
app.get('/style.css', (req, res) => {
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
|
||||
@@ -44,7 +45,7 @@ function startServer() {
|
||||
env['COLORTERM'] = 'truecolor';
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
term = pty.spawn(process.platform === 'win32' ? 'pwsh.exe' : 'bash', [], {
|
||||
name: 'xterm-256color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
@@ -55,9 +56,9 @@ function startServer() {
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
unsentOutput[term.pid] = '';
|
||||
temporaryDisposable[term.pid] = term.onData(function(data) {
|
||||
unsentOutput[term.pid] += data;
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
@@ -77,15 +78,29 @@ function startServer() {
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
temporaryDisposable[term.pid].dispose();
|
||||
delete temporaryDisposable[term.pid];
|
||||
ws.send(unsentOutput[term.pid]);
|
||||
delete unsentOutput[term.pid];
|
||||
|
||||
// unbuffered delivery after user input
|
||||
let userInput = false;
|
||||
|
||||
// string message buffering
|
||||
function buffer(socket, timeout) {
|
||||
function buffer(socket, timeout, maxSize) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
s += data;
|
||||
if (!sender) {
|
||||
if (s.length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
socket.send(s);
|
||||
s = '';
|
||||
if (sender) {
|
||||
clearTimeout(sender);
|
||||
sender = null;
|
||||
}
|
||||
} else if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(s);
|
||||
s = '';
|
||||
@@ -95,29 +110,38 @@ function startServer() {
|
||||
};
|
||||
}
|
||||
// binary message buffering
|
||||
function bufferUtf8(socket, timeout) {
|
||||
let buffer = [];
|
||||
let sender = null;
|
||||
function bufferUtf8(socket, timeout, maxSize) {
|
||||
const chunks = [];
|
||||
let length = 0;
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
buffer.push(data);
|
||||
chunks.push(data);
|
||||
length += data.length;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(buffer, length));
|
||||
buffer = [];
|
||||
if (length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
socket.send(Buffer.concat(chunks));
|
||||
chunks.length = 0;
|
||||
length = 0;
|
||||
if (sender) {
|
||||
clearTimeout(sender);
|
||||
sender = null;
|
||||
}
|
||||
} else if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(chunks));
|
||||
chunks.length = 0;
|
||||
length = 0;
|
||||
sender = null;
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 3, 262144);
|
||||
|
||||
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
|
||||
// it could flood the communication channel and make the terminal unresponsive. Learn more about
|
||||
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
|
||||
term.on('data', function(data) {
|
||||
term.onData(function(data) {
|
||||
try {
|
||||
send(data);
|
||||
} catch (ex) {
|
||||
@@ -126,13 +150,13 @@ function startServer() {
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
userInput = true;
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+17
-1
@@ -45,7 +45,6 @@ pre {
|
||||
|
||||
#container {
|
||||
display: flex;
|
||||
height: 75vh;
|
||||
}
|
||||
#grid {
|
||||
flex: 1;
|
||||
@@ -90,3 +89,20 @@ pre {
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#texture-atlas-zoom:checked + label + #texture-atlas canvas {
|
||||
/* Zoom atlas to the width of the container*/
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
#texture-atlas {
|
||||
width: 100%;
|
||||
}
|
||||
#texture-atlas canvas {
|
||||
image-rendering: pixelated;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.vt-button * {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
+6
-5
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "4.19.0",
|
||||
"version": "5.2.1",
|
||||
"main": "lib/xterm.js",
|
||||
"style": "css/xterm.css",
|
||||
"types": "typings/xterm.d.ts",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@types/glob": "^7.2.0",
|
||||
"@types/jsdom": "^16.2.13",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^14.14.44",
|
||||
"@types/node": "^18.16.0",
|
||||
"@types/utf8": "^3.0.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/ws": "^8.2.0",
|
||||
@@ -68,19 +68,20 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"eslint": "^8.1.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.6",
|
||||
"express": "^4.17.1",
|
||||
"express-ws": "^5.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"jsdom": "^18.0.1",
|
||||
"mocha": "^9.2.0",
|
||||
"mocha": "^10.1.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
"playwright": "^1.22.1",
|
||||
"source-map-loader": "^3.0.0",
|
||||
"source-map-support": "^0.5.20",
|
||||
"ts-loader": "^9.1.2",
|
||||
"typescript": "^4.4.4",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "4.7",
|
||||
"utf8": "^3.0.0",
|
||||
"webpack": "^5.61.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user