Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e02c37e52 | ||
|
|
71ee3c7a33 | ||
|
|
009add2b4a | ||
|
|
fb4b662756 | ||
|
|
7b52b8ee45 | ||
|
|
c03cb2b016 | ||
|
|
29dd30a98d | ||
|
|
68c572345b | ||
|
|
5e628d7fc6 | ||
|
|
93aae670ba | ||
|
|
7c7ac86681 | ||
|
|
076ab0f49b | ||
|
|
5fc1c04bd6 | ||
|
|
525d498e9f | ||
|
|
2b5ebb2b7b | ||
|
|
2269e92c88 | ||
|
|
7c434a0566 | ||
|
|
31bbeca2d8 | ||
|
|
158f7a17a6 | ||
|
|
8d0fde2d7f | ||
|
|
4cd4597082 | ||
|
|
1624854bd7 | ||
|
|
c099cb2a3d | ||
|
|
1c57b98c9a | ||
|
|
a29ab32876 | ||
|
|
91456fff04 | ||
|
|
13af55f9fc | ||
|
|
66454d378b | ||
|
|
92d09f2ea1 | ||
|
|
fac5e5f02d | ||
|
|
4ed97cae3a | ||
|
|
7bd9f8ee38 | ||
|
|
2dd4dfda80 | ||
|
|
f7ac0d878b | ||
|
|
fe2723b585 | ||
|
|
7fecec6599 | ||
|
|
5b25c65521 | ||
|
|
cb977ab6b6 | ||
|
|
2b21f27ac2 | ||
|
|
ec984d25db | ||
|
|
7000447915 | ||
|
|
486d61736b | ||
|
|
a4683ab874 | ||
|
|
b26c53d7e2 | ||
|
|
86b8744838 | ||
|
|
6f04e00ea1 | ||
|
|
9eadaf7079 | ||
|
|
fbebabe2e6 | ||
|
|
48184cc94f | ||
|
|
923c28cb77 | ||
|
|
03c1906f13 | ||
|
|
2b7b4d6fea | ||
|
|
abae7b6210 | ||
|
|
12a862c277 | ||
|
|
38c6af9b46 | ||
|
|
2eaa9b3b69 | ||
|
|
37b0e4018c | ||
|
|
57db72d287 | ||
|
|
ec02bab97c | ||
|
|
94ed3e800a | ||
|
|
1e1ccba238 | ||
|
|
a0d0c923f3 | ||
|
|
aba16687b0 | ||
|
|
797a71695f | ||
|
|
55c5a8560c | ||
|
|
9e768a4c7c | ||
|
|
d367eb41d2 | ||
|
|
7c164efdcc | ||
|
|
bd76cdbd20 | ||
|
|
40e51aba6b | ||
|
|
f9dc26d413 | ||
|
|
2fe082beea | ||
|
|
2835976b81 | ||
|
|
1968e8ced4 | ||
|
|
1965daff50 | ||
|
|
0e3db591c0 | ||
|
|
224902af9f | ||
|
|
131f578367 | ||
|
|
6b794285c6 | ||
|
|
e78f85bf65 | ||
|
|
9be0b6d918 | ||
|
|
3aa31111de | ||
|
|
81baad6acd | ||
|
|
25ef47a78f | ||
|
|
7d7d593fee | ||
|
|
766ffde4ca | ||
|
|
4534b56c1f | ||
|
|
ee0da5e4c1 | ||
|
|
e5d60c0895 | ||
|
|
2268ae3967 | ||
|
|
921da1a2ea | ||
|
|
ebd4092bee | ||
|
|
b6fd7a2b93 | ||
|
|
210ba8109c | ||
|
|
317ccdac66 | ||
|
|
43a3251bee | ||
|
|
94ab56542f | ||
|
|
92f1881dfe | ||
|
|
ec4c018f73 | ||
|
|
2c25dd1970 | ||
|
|
06fbe69b09 | ||
|
|
4bd154e7dc | ||
|
|
516756fad8 | ||
|
|
2b3cfb39a1 | ||
|
|
e9cbb3e547 | ||
|
|
1744801867 | ||
|
|
64cb9a3c62 | ||
|
|
0e947788e0 | ||
|
|
60b59db94f | ||
|
|
6d7032a906 | ||
|
|
b452fc9e85 | ||
|
|
dac4dec0ad | ||
|
|
eae79e93f3 | ||
|
|
ee33765932 | ||
|
|
8af83fef85 | ||
|
|
6f0a5c9f0c | ||
|
|
1529e0bf04 | ||
|
|
6021e6cf81 | ||
|
|
427d33c7b7 | ||
|
|
95f332cb13 | ||
|
|
db4567599b | ||
|
|
0667c7aedd | ||
|
|
1c5596de66 | ||
|
|
905707284d | ||
|
|
67f46a54b8 | ||
|
|
b8db3853f1 | ||
|
|
a9015c0ee6 | ||
|
|
60db1e7f82 | ||
|
|
6dae811dae | ||
|
|
a20599efb4 | ||
|
|
27b8d73f66 | ||
|
|
025e8505d5 | ||
|
|
16f5900252 | ||
|
|
e2d699cb32 | ||
|
|
156f75628a | ||
|
|
8fad65d1d5 | ||
|
|
0867804504 | ||
|
|
f0900fdc5f | ||
|
|
0be744b567 | ||
|
|
243b0bb626 | ||
|
|
0b32b42134 | ||
|
|
ef1cd4318b | ||
|
|
6e39d03a52 | ||
|
|
7b7dbd36d6 | ||
|
|
d03b5c9481 | ||
|
|
93e060aeb2 | ||
|
|
a84351ad0c | ||
|
|
25a78c77f2 | ||
|
|
a35fa611ff | ||
|
|
0b54c9a5d6 | ||
|
|
3b3b23a2fd | ||
|
|
7b874473d3 | ||
|
|
9d7b9293eb | ||
|
|
957e3e0f08 | ||
|
|
2f0a7eee8d | ||
|
|
1a3e14a24c | ||
|
|
a6b67fb8a6 | ||
|
|
843bdc4d55 | ||
|
|
2aee19f0e1 | ||
|
|
9f3be5e28d | ||
|
|
f69f803f0f | ||
|
|
2eca26b210 | ||
|
|
2ca0ccbe0c | ||
|
|
c908eeed5c | ||
|
|
caa1223e5c | ||
|
|
62cf3d39e1 | ||
|
|
2437b68d5d | ||
|
|
09fc798e80 | ||
|
|
7ae6febedc | ||
|
|
39ae0f45fd | ||
|
|
855e911398 | ||
|
|
73a77cda3d | ||
|
|
6c9346011d | ||
|
|
28c4732abc | ||
|
|
1cef30cca0 | ||
|
|
af787e1c85 | ||
|
|
70f6fa2bc9 | ||
|
|
bef87403db | ||
|
|
5890029f71 | ||
|
|
1f4fe0a8c0 | ||
|
|
519f6a3d4a | ||
|
|
530f758905 | ||
|
|
fb7349bc87 | ||
|
|
dc390ab03a | ||
|
|
6f6cc6a686 | ||
|
|
136e2f2afe | ||
|
|
73bb72dbcf | ||
|
|
4b2f774f53 | ||
|
|
0a4151fbfa | ||
|
|
07d187151e | ||
|
|
47f606f0df | ||
|
|
18125dad84 | ||
|
|
f317b96262 | ||
|
|
c3d8c2a919 | ||
|
|
5c1e6dce99 | ||
|
|
0321bcb715 | ||
|
|
bb5d61fb14 | ||
|
|
3b3012353f | ||
|
|
adad53d526 | ||
|
|
1b4460e37d | ||
|
|
482dad3036 | ||
|
|
7276027861 | ||
|
|
62055522fb | ||
|
|
0366aa2217 | ||
|
|
2b79b7b29f | ||
|
|
02243173c2 | ||
|
|
a84a95055d | ||
|
|
b8bf35be17 | ||
|
|
3ba579f680 | ||
|
|
4f41070b45 | ||
|
|
d2031955ec | ||
|
|
91c08ed48e | ||
|
|
acf53f69e0 | ||
|
|
bb237f109a | ||
|
|
44d4ec83fc | ||
|
|
33124a31bd | ||
|
|
1e90aab2a6 | ||
|
|
44ced27bee | ||
|
|
c3def09c38 | ||
|
|
afff339d28 | ||
|
|
b7a597f925 | ||
|
|
ae84ee39f3 | ||
|
|
73e60586c9 | ||
|
|
b56bc77dcc | ||
|
|
32a852c0bf | ||
|
|
40e622dfc8 | ||
|
|
994861a6fa | ||
|
|
b770431102 | ||
|
|
2fd108c864 | ||
|
|
914f7be644 | ||
|
|
1fdb8c006d | ||
|
|
b46404b139 | ||
|
|
1dfd14fbc0 | ||
|
|
a1ddc629dc | ||
|
|
9cfc2c881c | ||
|
|
b7f9db69ca | ||
|
|
60eb4f3385 | ||
|
|
41e97ec042 | ||
|
|
fe838726b0 | ||
|
|
1cfce3d0b6 | ||
|
|
f64cb1566d | ||
|
|
9b204c012a | ||
|
|
6b33ac1e85 | ||
|
|
18e578bd09 | ||
|
|
e8a925b6af | ||
|
|
db6e0cd2ef | ||
|
|
f6738ec354 | ||
|
|
1067ec19a8 | ||
|
|
e7a7842808 | ||
|
|
ce3bb35c8c | ||
|
|
2d7b531bba | ||
|
|
2d81490ac4 | ||
|
|
5939d72d8f | ||
|
|
d1bd4973ad | ||
|
|
f407f0c5a6 | ||
|
|
40eccfa61a | ||
|
|
c79aa542c9 | ||
|
|
366ffedf08 | ||
|
|
c796514de8 | ||
|
|
e75ec5b51f | ||
|
|
702c3578a3 | ||
|
|
0eecf8187d | ||
|
|
e9058b7498 | ||
|
|
25b266e809 | ||
|
|
2e638598e4 | ||
|
|
8562635231 | ||
|
|
b358754b46 | ||
|
|
64d04f0712 | ||
|
|
ebdafe54aa | ||
|
|
829ecac8e3 | ||
|
|
d22e3e093a | ||
|
|
a9662e8b7b | ||
|
|
5f98db9b76 | ||
|
|
664a110fd1 | ||
|
|
3b5044b55b | ||
|
|
e848151b50 | ||
|
|
91197bd1aa | ||
|
|
d1fdaa17d6 | ||
|
|
5660c404f0 | ||
|
|
79c1fff853 | ||
|
|
962c070a75 | ||
|
|
5bdd5022b2 | ||
|
|
370bad1380 | ||
|
|
17b8247c3d | ||
|
|
c0d1dec847 | ||
|
|
5889344dd2 | ||
|
|
5f80fe00d5 | ||
|
|
b225b9a710 | ||
|
|
72c51b19a5 | ||
|
|
679c149164 | ||
|
|
92a18defce | ||
|
|
859c9ec68d | ||
|
|
b4aeb1b3e7 | ||
|
|
8f1313519f | ||
|
|
1c0a25d01b | ||
|
|
a5df5e7333 | ||
|
|
4bd3e1d0c2 | ||
|
|
ad145541af | ||
|
|
37153018b9 | ||
|
|
90f1318596 | ||
|
|
4a5ae78d35 | ||
|
|
097879b839 | ||
|
|
2d9ad32d93 | ||
|
|
4d257f469b | ||
|
|
521dc915dc | ||
|
|
5ea669f091 | ||
|
|
aa6e8d91c2 | ||
|
|
089dc282c3 | ||
|
|
329b465a63 | ||
|
|
6785dc19ab | ||
|
|
c515cc2823 | ||
|
|
848f89e8a7 | ||
|
|
bd7f76190c | ||
|
|
514afcd445 | ||
|
|
3e13abf974 | ||
|
|
4cfe32f626 | ||
|
|
f36954764d | ||
|
|
1c98037eed | ||
|
|
2d732e3a99 | ||
|
|
53d105714a | ||
|
|
78aad113a6 | ||
|
|
d2c28c819f | ||
|
|
3d75333e95 | ||
|
|
7e5bdb5321 | ||
|
|
be74969d70 | ||
|
|
196f863441 | ||
|
|
823258e311 | ||
|
|
e100a4310b | ||
|
|
681c912533 | ||
|
|
6339c09c01 | ||
|
|
96634298e4 | ||
|
|
65568f502e | ||
|
|
1fb9f0185a | ||
|
|
74c5256d6d | ||
|
|
3991b5f251 | ||
|
|
9afb924e84 | ||
|
|
7a82e7370f | ||
|
|
709e9fd01a | ||
|
|
e16f87f973 | ||
|
|
d4ddac704c | ||
|
|
bc92d61616 | ||
|
|
4884d81704 | ||
|
|
d99bdb7710 | ||
|
|
eaa265b4fa | ||
|
|
8fa3cae618 | ||
|
|
0027b27ea3 | ||
|
|
2bcc653293 | ||
|
|
ebfff5efee | ||
|
|
ac0207bf2e | ||
|
|
8068880aab | ||
|
|
064fe0cfd9 | ||
|
|
096fe17135 | ||
|
|
4d501b38b8 | ||
|
|
f79e9a1951 | ||
|
|
64a62fb0c1 | ||
|
|
2c45989fea | ||
|
|
f779e4ddc0 | ||
|
|
7be0016e45 | ||
|
|
ac923e55d2 | ||
|
|
e08e5607ed | ||
|
|
2ac2eeadfc | ||
|
|
ffadc0b31d | ||
|
|
cc1089e943 | ||
|
|
47c21df83d | ||
|
|
380e6805e8 | ||
|
|
f616ebb083 | ||
|
|
ce2d115087 | ||
|
|
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 | ||
|
|
b7d73f63cd | ||
|
|
ee26780e03 | ||
|
|
b35415ed16 | ||
|
|
2a9ec8fd1d | ||
|
|
2fdb46919c | ||
|
|
c8e633ecb5 | ||
|
|
c546f7cc8a | ||
|
|
6997f1a940 | ||
|
|
14c6df3187 | ||
|
|
18be346df9 | ||
|
|
37297e09ed | ||
|
|
b2f0cc8dc1 | ||
|
|
6cbee3349e | ||
|
|
f0edc1b613 | ||
|
|
cd9867e222 | ||
|
|
e1d852c86a | ||
|
|
455c744ab1 | ||
|
|
21f48ba803 | ||
|
|
49fdacba6a | ||
|
|
a359c7ca10 | ||
|
|
242eade757 | ||
|
|
4037b943b9 | ||
|
|
a8cec5f02f | ||
|
|
9154831afa | ||
|
|
d6599bc048 | ||
|
|
8a1faed5cb | ||
|
|
72a157df5e | ||
|
|
6daadc4c55 | ||
|
|
c05d0f650c | ||
|
|
d5b5f1a9a4 | ||
|
|
dfcd674f8c | ||
|
|
a7e0a9c534 | ||
|
|
5839accbf1 | ||
|
|
01c9a98e8e | ||
|
|
bcfed71e1d | ||
|
|
046d4b8314 | ||
|
|
a9da61e605 | ||
|
|
24c7be210c | ||
|
|
9c815250b6 | ||
|
|
377f88796f | ||
|
|
ed6c849501 | ||
|
|
5a21948b4a | ||
|
|
b24bda92ad | ||
|
|
9895691e0e | ||
|
|
77870b06b9 | ||
|
|
41dc93a008 | ||
|
|
79d2f738a2 | ||
|
|
7701557b99 | ||
|
|
ba9f2e0f11 | ||
|
|
b3fb8db6b3 | ||
|
|
91946dcd9b | ||
|
|
41c0e26292 | ||
|
|
fe89817667 | ||
|
|
7ee7aa2719 | ||
|
|
b05c26c6fd | ||
|
|
0adbedf98d | ||
|
|
89ead0b4b7 | ||
|
|
349510065b | ||
|
|
fa8a0e7f24 | ||
|
|
0c4e9c6b27 | ||
|
|
de2b62eeac | ||
|
|
65b2af10f8 | ||
|
|
1a20fbb25f | ||
|
|
d51ba2578b | ||
|
|
56734874ac | ||
|
|
72f1d1048a | ||
|
|
d17df65fad | ||
|
|
ed8c2269cb | ||
|
|
f1b47485cd | ||
|
|
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,15 +7,20 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"demo/tsconfig.json",
|
||||
"src/browser/tsconfig.json",
|
||||
"src/common/tsconfig.json",
|
||||
"src/headless/tsconfig.json",
|
||||
"test/api/tsconfig.json",
|
||||
"test/benchmark/tsconfig.json",
|
||||
"test/playwright/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-image/src/tsconfig.json",
|
||||
"addons/xterm-addon-image/test/tsconfig.json",
|
||||
"addons/xterm-addon-ligatures/src/tsconfig.json",
|
||||
"addons/xterm-addon-search/src/tsconfig.json",
|
||||
"addons/xterm-addon-search/test/tsconfig.json",
|
||||
@@ -32,12 +37,14 @@
|
||||
"sourceType": "module"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/inwasm-sdks/*",
|
||||
"**/typings/*.d.ts",
|
||||
"**/node_modules",
|
||||
"**/*.js"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-extra-semi": "error",
|
||||
@@ -84,7 +91,12 @@
|
||||
],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{ "selector": "default", "format": ["camelCase"] },
|
||||
{ "selector": "default", "format": ["camelCase"],
|
||||
"filter": {
|
||||
"regex": "^[a-z]",
|
||||
"match": true
|
||||
}
|
||||
},
|
||||
// variableLike
|
||||
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
|
||||
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
|
||||
@@ -93,12 +105,34 @@
|
||||
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
||||
// memberLike - Allow enum-like objects to use UPPER_CASE
|
||||
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"],
|
||||
"filter": {
|
||||
"regex": "^[a-z]",
|
||||
"match": true
|
||||
}
|
||||
},
|
||||
// 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"] }
|
||||
],
|
||||
"@typescript-eslint/no-confusing-void-expression": [
|
||||
"warn",
|
||||
{ "ignoreArrowShorthand": true }
|
||||
],
|
||||
"@typescript-eslint/no-useless-constructor": "warn",
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
@@ -127,7 +161,20 @@
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"jsdoc/no-multi-asterisks": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 1000, // Don't enforce for code
|
||||
"comments": 100,
|
||||
"ignoreTrailingComments": true,
|
||||
"ignoreUrls": true,
|
||||
"ignorePattern": "^ *((?<vt_comment>(//|\\*) @vt)|(?<table_comment>\\* \\| )|(?<commented_code>// ))"
|
||||
}
|
||||
],
|
||||
"new-parens": "warn",
|
||||
"no-duplicate-imports": "warn",
|
||||
"no-else-return": [
|
||||
@@ -146,8 +193,35 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"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-unused-vars": ["warn", {
|
||||
"vars": "all",
|
||||
"args": "none"
|
||||
}],
|
||||
"no-var": "warn",
|
||||
"one-var": [
|
||||
"warn",
|
||||
@@ -169,9 +243,14 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.test.ts"],
|
||||
"files": [
|
||||
"**/*.api.ts",
|
||||
"**/*.test.ts"
|
||||
],
|
||||
"rules": {
|
||||
"object-curly-spacing": "off"
|
||||
"object-curly-spacing": "off",
|
||||
"max-len": "off",
|
||||
"no-unused-vars": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-extra-semi": "error",
|
||||
"@typescript-eslint/array-type": [
|
||||
"warn",
|
||||
{
|
||||
"default": "array",
|
||||
"readonly": "generic"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/indent": [
|
||||
"warn",
|
||||
2
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"warn",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "comma",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"warn",
|
||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"warn",
|
||||
"single",
|
||||
{ "allowTemplateLiterals": true }
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"objects": "never",
|
||||
"arrays": "never",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"curly": [
|
||||
"warn",
|
||||
"multi-line"
|
||||
],
|
||||
"eol-last": "warn",
|
||||
"eqeqeq": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 1000, // Don't enforce for code
|
||||
"comments": 80,
|
||||
"ignoreUrls": true,
|
||||
"ignorePattern": "^ *(?<ps_description>\\* Ps=)"
|
||||
}
|
||||
],
|
||||
"no-irregular-whitespace": "warn",
|
||||
"no-trailing-spaces": "warn",
|
||||
"object-curly-spacing": [
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"spaced-comment": [
|
||||
"warn",
|
||||
"always",
|
||||
{
|
||||
"markers": ["/"],
|
||||
"exceptions": ["-"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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,300 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Build
|
||||
run: yarn setup
|
||||
- name: Zip artifacts
|
||||
run: |
|
||||
zip -r compressed-build \
|
||||
./out/* \
|
||||
./out-test/* \
|
||||
./addons/xterm-addon-attach/out/* \
|
||||
./addons/xterm-addon-attach/out-test/* \
|
||||
./addons/xterm-addon-canvas/out/* \
|
||||
./addons/xterm-addon-canvas/out-test/* \
|
||||
./addons/xterm-addon-fit/out/* \
|
||||
./addons/xterm-addon-fit/out-test/* \
|
||||
./addons/xterm-addon-image/out/* \
|
||||
./addons/xterm-addon-image/out-test/* \
|
||||
./addons/xterm-addon-ligatures/out/* \
|
||||
./addons/xterm-addon-ligatures/out-test/* \
|
||||
./addons/xterm-addon-search/out/* \
|
||||
./addons/xterm-addon-search/out-test/* \
|
||||
./addons/xterm-addon-serialize/out/* \
|
||||
./addons/xterm-addon-serialize/out-test/* \
|
||||
./addons/xterm-addon-unicode11/out/* \
|
||||
./addons/xterm-addon-unicode11/out-test/* \
|
||||
./addons/xterm-addon-web-links/out/* \
|
||||
./addons/xterm-addon-web-links/out-test/* \
|
||||
./addons/xterm-addon-webgl/out/* \
|
||||
./addons/xterm-addon-webgl/out-test/*
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
path: compressed-build.zip
|
||||
if-no-files-found: error
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- name: Lint code
|
||||
run: yarn lint
|
||||
- name: Lint API
|
||||
run: yarn lint-api
|
||||
|
||||
test-unit-coverage:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Unit test coverage
|
||||
run: |
|
||||
yarn test-unit-coverage --forbid-only
|
||||
EXIT_CODE=$?
|
||||
./node_modules/.bin/nyc report --reporter=cobertura
|
||||
exit $EXIT_CODE
|
||||
|
||||
test-unit-parallel:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18]
|
||||
runs-on: [ubuntu, macos, windows]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- name: Wait for build job
|
||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job: build
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Unit tests
|
||||
run: yarn test-unit --forbid-only
|
||||
|
||||
test-unit:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16]
|
||||
runs-on: [ubuntu, macos, windows]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Unit tests
|
||||
run: yarn test-unit --forbid-only
|
||||
|
||||
test-api-parallel:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18] # just one as integration tests are about testing in browser
|
||||
runs-on: [ubuntu] # macos is flaky
|
||||
browser: [chromium, firefox]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- name: Install playwright
|
||||
run: npx playwright install
|
||||
- name: Wait for build job
|
||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job: build
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Integration tests (${{ matrix.browser }})
|
||||
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
|
||||
|
||||
test-playwright-parallel:
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18] # just one as integration tests are about testing in browser
|
||||
runs-on: [ubuntu] # macos is flaky
|
||||
browser: [chromium, firefox, webkit]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- name: Install playwright
|
||||
run: npx playwright install --with-deps ${{ matrix.browser }}
|
||||
- name: Wait for build job
|
||||
uses: NathanFirmo/wait-for-other-job@v1.1.1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
job: build
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Build demo
|
||||
run: yarn build-demo
|
||||
- name: Integration tests
|
||||
run: yarn test-playwright-${{ matrix.browser }} --forbid-only
|
||||
|
||||
test-api:
|
||||
needs: build
|
||||
timeout-minutes: 20
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18] # just one as integration tests are about testing in browser
|
||||
runs-on: [windows] # macos is flaky
|
||||
browser: [chromium, firefox]
|
||||
runs-on: ${{ matrix.runs-on }}-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn --frozen-lockfile
|
||||
yarn install-addons
|
||||
- name: Install playwright
|
||||
run: npx playwright install
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: build-artifacts
|
||||
- name: Unzip artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "$RUNNER_OS" == "Windows" ]; then
|
||||
pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}"
|
||||
else
|
||||
unzip -o compressed-build.zip
|
||||
fi
|
||||
ls -R
|
||||
- name: Integration tests (${{ matrix.browser }})
|
||||
run: yarn test-api-${{ matrix.browser }} --headless --forbid-only
|
||||
@@ -0,0 +1,35 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master" ]
|
||||
pull_request:
|
||||
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' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v3
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v2
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
# If a commit reaches master, assume it has passed CI via PR and publish
|
||||
# without running tests to save time to publish
|
||||
branches: [ "master" ]
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 18.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
- name: Install dependencies
|
||||
run: yarn --frozen-lockfile
|
||||
- name: Build
|
||||
run: yarn setup
|
||||
- name: Package headless
|
||||
run: |
|
||||
yarn package-headless
|
||||
node ./bin/package_headless.js
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: node ./bin/publish.js
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
{
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
"typescript.preferences.quoteStyle": "single",
|
||||
"mochaExplorer.envPath": ".mocha.env"
|
||||
"files.associations": {
|
||||
".eslintrc.json.typings": "jsonc"
|
||||
},
|
||||
"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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -28,6 +28,10 @@ opening an issue, read these pointers.
|
||||
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||
bottom of the pile.
|
||||
|
||||
## Answering discussion questions
|
||||
|
||||
Issues are only meant to track (likely) feature requests and bugs. We use [GitHub Discussions](https://github.com/xtermjs/xterm.js/discussions) for general Q&A as well as discussing possible features. If you want to help out, many questions get asked over at the [discussions page](https://github.com/xtermjs/xterm.js/discussions) which could use an expert as the core team is often stretched thin.
|
||||
|
||||
## Contributing code
|
||||
|
||||
You can find issues to work on by looking at the [help wanted](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) or [good first issue](https://github.com/xtermjs/xterm.js/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) issues. It's a good idea to comment on the issue saying that you're taking it, just in case someone else comes along and you duplicate work. Once you have your issue, here are the steps to contribute:
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
|
||||
Xterm.js is a front-end component written in TypeScript that lets applications bring fully-featured terminals to their users in the browser. It's used by popular projects such as VS Code, Hyper and Theia.
|
||||
|
||||
## Features
|
||||
@@ -87,8 +85,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 +97,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 +158,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 +184,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 +208,23 @@ 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.
|
||||
- [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab.
|
||||
- [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor.
|
||||
- [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments
|
||||
- [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.9.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"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import WebSocket = require('ws');
|
||||
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
@@ -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.5.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,499 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
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 { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IRenderLayer } from './Types';
|
||||
|
||||
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;
|
||||
protected _charAtlasDisposable = this.register(new MutableDisposable());
|
||||
|
||||
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();
|
||||
}));
|
||||
}
|
||||
|
||||
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._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlasDisposable.value = 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);
|
||||
|
||||
if (!this._charAtlas) {
|
||||
return;
|
||||
}
|
||||
|
||||
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, true);
|
||||
} else {
|
||||
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, true);
|
||||
}
|
||||
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,68 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
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 { setTraceLogger } from 'common/services/LogService';
|
||||
import { IBufferService, IDecorationService, ILogService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
import { CanvasRenderer } from './CanvasRenderer';
|
||||
|
||||
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 logService: ILogService = unsafeCore._logService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
// Set trace logger just in case it hasn't been yet which could happen when the addon is
|
||||
// bundled separately to the core module
|
||||
setTraceLogger(logService);
|
||||
|
||||
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 { ILinkifier2 } from 'browser/Types';
|
||||
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 { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
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 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CursorBlinkStateManager } from 'browser/renderer/shared/CursorBlinkStateManager';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { MutableDisposable } from 'common/Lifecycle';
|
||||
import { isFirefox } from 'common/Platform';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
y: number;
|
||||
isFocused: boolean;
|
||||
style: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _state: ICursorState;
|
||||
private _cursorRenderers: {[key: string]: (x: number, y: number, cell: ICellData) => void};
|
||||
private _cursorBlinkStateManager: MutableDisposable<CursorBlinkStateManager> = this.register(new MutableDisposable());
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _coreService: ICoreService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
this._cursorRenderers = {
|
||||
'bar': this._renderBarCursor.bind(this),
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this),
|
||||
'outline': this._renderOutlineCursor.bind(this)
|
||||
};
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this._handleOptionsChanged();
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
super.resize(dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
this._clearCursor();
|
||||
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
||||
this._handleOptionsChanged();
|
||||
}
|
||||
|
||||
public handleBlur(): void {
|
||||
this._cursorBlinkStateManager.value?.pause();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public handleFocus(): void {
|
||||
this._cursorBlinkStateManager.value?.resume();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
private _handleOptionsChanged(): void {
|
||||
if (this._optionsService.rawOptions.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager.value) {
|
||||
this._cursorBlinkStateManager.value = new CursorBlinkStateManager(() => this._render(true), this._coreBrowserService);
|
||||
}
|
||||
} else {
|
||||
this._cursorBlinkStateManager.clear();
|
||||
}
|
||||
// Request a refresh from the terminal as management of rendering is being
|
||||
// moved back to the terminal
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public handleCursorMove(): void {
|
||||
this._cursorBlinkStateManager.value?.restartBlinkAnimation();
|
||||
}
|
||||
|
||||
public handleGridChanged(startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager.value || this._cursorBlinkStateManager.value.isPaused) {
|
||||
this._render(false);
|
||||
} else {
|
||||
this._cursorBlinkStateManager.value.restartBlinkAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
private _render(triggeredByAnimationFrame: boolean): void {
|
||||
// Don't draw the cursor if it's hidden
|
||||
if (!this._coreService.isCursorInitialized || this._coreService.isCursorHidden) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
const cursorY = this._bufferService.buffer.ybase + this._bufferService.buffer.y;
|
||||
const viewportRelativeCursorY = cursorY - this._bufferService.buffer.ydisp;
|
||||
|
||||
// Don't draw the cursor if it's off-screen
|
||||
if (viewportRelativeCursorY < 0 || viewportRelativeCursorY >= this._bufferService.rows) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
// in case cursor.x == cols adjust visual cursor to cols - 1
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
this._bufferService.buffer.lines.get(cursorY)!.loadCell(cursorX, this._cell);
|
||||
if (this._cell.content === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
const cursorInactiveStyle = this._optionsService.rawOptions.cursorInactiveStyle;
|
||||
if (cursorInactiveStyle && cursorInactiveStyle !== 'none') {
|
||||
this._cursorRenderers[cursorInactiveStyle](cursorX, viewportRelativeCursorY, this._cell);
|
||||
}
|
||||
this._ctx.restore();
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't draw the cursor if it's blinking
|
||||
if (this._cursorBlinkStateManager.value && !this._cursorBlinkStateManager.value.isCursorVisible) {
|
||||
this._clearCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._state) {
|
||||
// The cursor is already in the correct spot, don't redraw
|
||||
if (this._state.x === cursorX &&
|
||||
this._state.y === viewportRelativeCursorY &&
|
||||
this._state.isFocused === this._coreBrowserService.isFocused &&
|
||||
this._state.style === this._optionsService.rawOptions.cursorStyle &&
|
||||
this._state.width === this._cell.getWidth()) {
|
||||
return;
|
||||
}
|
||||
this._clearCursor();
|
||||
}
|
||||
|
||||
this._ctx.save();
|
||||
this._cursorRenderers[this._optionsService.rawOptions.cursorStyle || 'block'](cursorX, viewportRelativeCursorY, this._cell);
|
||||
this._ctx.restore();
|
||||
|
||||
this._state.x = cursorX;
|
||||
this._state.y = viewportRelativeCursorY;
|
||||
this._state.isFocused = false;
|
||||
this._state.style = this._optionsService.rawOptions.cursorStyle;
|
||||
this._state.width = this._cell.getWidth();
|
||||
}
|
||||
|
||||
private _clearCursor(): void {
|
||||
if (this._state) {
|
||||
// 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);
|
||||
}
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
isFocused: false,
|
||||
style: '',
|
||||
width: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
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._themeService.colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
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._themeService.colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderOutlineCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { GridCache } from './GridCache';
|
||||
|
||||
describe('GridCache', () => {
|
||||
let grid: GridCache<number>;
|
||||
@@ -3,33 +3,33 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/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 { ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
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 { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
@@ -3,17 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from 'browser/renderer/GridCache';
|
||||
import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content } from 'common/buffer/Constants';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Content, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { GridCache } from './GridCache';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -30,18 +30,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 +51,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 +93,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 +174,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 +233,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 +251,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 +289,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);
|
||||
// }
|
||||
}
|
||||
@@ -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.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
/**
|
||||
@@ -23,8 +24,6 @@ const MINIMUM_ROWS = 1;
|
||||
export class FitAddon implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
|
||||
constructor() {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
}
|
||||
@@ -58,8 +57,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 +81,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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from 'playwright';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
@@ -41,14 +41,13 @@ describe('FitAddon', () => {
|
||||
});
|
||||
|
||||
describe('proposeDimensions', () => {
|
||||
afterEach(async () => {
|
||||
return await unloadFit();
|
||||
});
|
||||
afterEach(() => unloadFit());
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
await loadFit();
|
||||
const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`);
|
||||
assert.equal(dimensions.cols, 87);
|
||||
assert.isAbove(dimensions.cols, 85);
|
||||
assert.isBelow(dimensions.cols, 88);
|
||||
assert.isAbove(dimensions.rows, 24);
|
||||
assert.isBelow(dimensions.rows, 29);
|
||||
});
|
||||
@@ -56,7 +55,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);
|
||||
});
|
||||
@@ -80,16 +80,15 @@ describe('FitAddon', () => {
|
||||
});
|
||||
|
||||
describe('fit', () => {
|
||||
afterEach(async () => {
|
||||
return await unloadFit();
|
||||
});
|
||||
afterEach(() => unloadFit());
|
||||
|
||||
it('default', async function(): Promise<any> {
|
||||
await loadFit();
|
||||
await page.evaluate(`window.fit.fit()`);
|
||||
const cols: number = await page.evaluate(`window.term.cols`);
|
||||
const rows: number = await page.evaluate(`window.term.rows`);
|
||||
assert.equal(cols, 87);
|
||||
assert.isAbove(cols, 85);
|
||||
assert.isBelow(cols, 88);
|
||||
assert.isAbove(rows, 24);
|
||||
assert.isBelow(rows, 29);
|
||||
});
|
||||
@@ -99,7 +98,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"
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
node_modules/
|
||||
*.swp
|
||||
.lock-wscript
|
||||
lib/
|
||||
out/
|
||||
out-test/
|
||||
out-worker/
|
||||
.nyc_output/
|
||||
Makefile.gyp
|
||||
*.Makefile
|
||||
*.target.gyp.mk
|
||||
*.node
|
||||
example/*.log
|
||||
docs/
|
||||
npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
build/
|
||||
.DS_Store
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
demo/dist/
|
||||
|
||||
# skip benchmark & inwasm folders, except builds
|
||||
.benchmark/
|
||||
timeline/
|
||||
inwasm-sdks/
|
||||
inwasm-builds/
|
||||
!inwasm-builds/**/final.wat
|
||||
!inwasm-builds/**/final.wasm
|
||||
!inwasm-builds/**/definition.json
|
||||
@@ -0,0 +1,10 @@
|
||||
.github
|
||||
fixture
|
||||
overwrite
|
||||
bootstrap.sh
|
||||
tsconfig.json
|
||||
webpack.config.js
|
||||
test
|
||||
out-test
|
||||
inwasm-sdks
|
||||
inwasm-builds
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2019, 2020, 2021, 2022, 2023 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,231 @@
|
||||
## xterm-addon-image
|
||||
|
||||
Inline image output in xterm.js. Supports SIXEL and iTerm's inline image protocol (IIP).
|
||||
|
||||
|
||||

|
||||
|
||||
|
||||
### Install from npm
|
||||
|
||||
```bash
|
||||
npm install --save xterm-addon-image
|
||||
```
|
||||
|
||||
|
||||
### Usage
|
||||
|
||||
```ts
|
||||
import { Terminal } from 'xterm';
|
||||
import { ImageAddon, IImageAddonOptions } from 'xterm-addon-image';
|
||||
|
||||
// customize as needed (showing addon defaults)
|
||||
const customSettings: IImageAddonOptions = {
|
||||
enableSizeReports: true, // whether to enable CSI t reports (see below)
|
||||
pixelLimit: 16777216, // max. pixel size of a single image
|
||||
sixelSupport: true, // enable sixel support
|
||||
sixelScrolling: true, // whether to scroll on image output
|
||||
sixelPaletteLimit: 256, // initial sixel palette size
|
||||
sixelSizeLimit: 25000000, // size limit of a single sixel sequence
|
||||
storageLimit: 128, // FIFO storage limit in MB
|
||||
showPlaceholder: true, // whether to show a placeholder for evicted images
|
||||
iipSupport: true, // enable iTerm IIP support
|
||||
iipSizeLimit: 20000000 // size limit of a single IIP sequence
|
||||
}
|
||||
|
||||
// initialization
|
||||
const terminal = new Terminal();
|
||||
const imageAddon = new ImageAddon(customSettings);
|
||||
terminal.loadAddon(imageAddon);
|
||||
```
|
||||
|
||||
### General Notes
|
||||
|
||||
- *IMPORTANT:* The worker approach as done in previous releases got removed.
|
||||
The addon contructor no longer expects a worker path as first argument.
|
||||
|
||||
- By default the addon will activate these `windowOptions` reports on the terminal:
|
||||
- getWinSizePixels (CSI 14 t)
|
||||
- getCellSizePixels (CSI 16 t)
|
||||
- getWinSizeChars (CSI 18 t)
|
||||
|
||||
to help applications getting useful terminal metrics for their image preparations. Set `enableSizeReports` in the constructor options to `false`, if you dont want the addon to alter these terminal settings. This is especially useful, if you have very strict security needs not allowing any terminal reports, or deal with `windowOptions` by other means.
|
||||
|
||||
|
||||
### Operation Modes
|
||||
|
||||
- **SIXEL Support**
|
||||
Set by default, change it with `{sixelSupport: true}`.
|
||||
|
||||
- **Scrolling On | Off**
|
||||
By default scrolling is on, thus an image will advance the cursor at the bottom if needed.
|
||||
(see cursor positioning).
|
||||
|
||||
If scrolling is off, the image gets painted from the top left of the current viewport
|
||||
and might be truncated if the image exceeds the viewport size.
|
||||
The cursor position does not change.
|
||||
|
||||
You can customize this behavior with the constructor option `{sixelScrolling: false}`
|
||||
or with `DECSET 80` (off, binary: `\x1b [ ? 80 h`) and
|
||||
`DECRST 80` (on, binary: `\x1b [ ? 80 l`) during runtime.
|
||||
|
||||
- **Cursor Positioning**
|
||||
If scrolling is set, the cursor will be placed at the first image column of the last image row (VT340 mode).
|
||||
Other cursor positioning modes as used by xterm or mintty are not supported.
|
||||
|
||||
- **SIXEL Palette Handling**
|
||||
By default the addon limits the palette size to 256 registers (as demanded by the DEC specification).
|
||||
The limit can be increased to a maximum of 4096 registers (via `sixelPaletteLimit`).
|
||||
|
||||
The default palette is a mixture of VT340 colors (lower 16 registers), xterm colors (up to 256) and zeros (up to 4096).
|
||||
There is no private/shared palette distinction, palette colors are always carried over from a previous sixel image.
|
||||
Restoring the default palette size and colors is possible with `XTSMGRAPHICS 1 ; 2` (binary: `\x1b[?1;2S`).
|
||||
It gets also restored automatically on RIS and DECSTR.
|
||||
|
||||
Other than on older terminals, the underlying SIXEL library applies colors immediately to individual pixels
|
||||
(*printer mode*), thus it is technically possible to use more colors in one image than the palette has color slots.
|
||||
This feature is called *high-color* in libsixel.
|
||||
|
||||
A terminal wide shared palette mode with late indexed coloring of the output is not supported,
|
||||
therefore palette animations cannot be used.
|
||||
|
||||
- **SIXEL Raster Attributes Handling**
|
||||
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extend. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extend. Both is discouraged by the >30 years old specification.
|
||||
|
||||
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
|
||||
|
||||
- **IIP Support (iTerm's Inline Image Protocol)**
|
||||
Set by default, change it with `{iipSupport: true}`.
|
||||
|
||||
The IIP implementation has the following features / restrictions (sequence will silently fail for unmet conditions):
|
||||
- Supported formats: PNG, JPEG and GIF
|
||||
- No animation support.
|
||||
- Image type hinting is not supported (always deducted from data header).
|
||||
- File download is not supported.
|
||||
- Filename gets parsed but not used.
|
||||
- Strict base64 handling as of RFC4648 §4 (standard alphabet, optional padding, no separator bytes allowed).
|
||||
- Payload size may not exceed CEIL(sizeParameter * 4 / 3).
|
||||
- Image scaling beyond terminal viewport size is allowed (e.g. `width=200%`).
|
||||
- Image pixel size is restricted by `pixelLimit` (pre- and post resizing).
|
||||
- Size parameter is restricted by `iipSizeLimit`.
|
||||
- Cursor positioning behaves the same as for sixel (see above).
|
||||
|
||||
|
||||
### Storage and Drawing Settings
|
||||
|
||||
The internal storage holds images up to `storageLimit` (in MB, calculated as 4-channel RBGA unpacked, default 100 MB). Once hit images get evicted by FIFO rules. Furthermore images on the alternate buffer will always be erased on buffer changes.
|
||||
|
||||
The addon exposes two properties to interact with the storage limits at runtime:
|
||||
- `storageLimit`
|
||||
Change the value to your needs at runtime. This is especially useful, if you have multiple terminal
|
||||
instances running, that all add to one upper memory limit.
|
||||
- `storageUsage`
|
||||
Inspect the current memory usage of the image storage.
|
||||
|
||||
By default the addon will show a placeholder pattern for evicted images that are still part
|
||||
of the terminal (e.g. in the scrollback). The pattern can be deactivated by toggling `showPlaceholder`.
|
||||
|
||||
### Image Data Retrieval
|
||||
|
||||
The addon provides the following API endpoints to retrieve raw image data as canvas:
|
||||
|
||||
- `getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined`
|
||||
Returns the canvas containing the original image data (not resized) at the given buffer position.
|
||||
The buffer position is the 0-based absolute index (including scrollback at top).
|
||||
|
||||
- `extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined`
|
||||
Returns a canvas containing the actual single tile image data (maybe resized) at the given buffer position.
|
||||
The buffer position is the 0-based absolute index (including scrollback at top).
|
||||
Note that the canvas gets created and data copied over for every call, thus it is not suitable for performance critical actions.
|
||||
|
||||
### Memory Usage
|
||||
|
||||
The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
|
||||
- Incomming image data chunk at `term.write` (terminal)
|
||||
`term.write` might stock up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
|
||||
- Sequence Parser (terminal)
|
||||
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
|
||||
- Sequence Handler - Chunk Decoding (addon)
|
||||
Image data chunks are processed immediately by the SIXEL decoder (streamlined). The decoder allocates memory for image
|
||||
pixels as needed. The allowed image size is restricted by `pixelLimit` (default 16M pixels), the decoder holds 2 pixel buffers at maximum during decoding (RGBA, ~128 MB for 16M pixels).
|
||||
- Sequence Handler - Image Finalization (addon)
|
||||
After decoding the final pixel buffer is grabbed by the sequence handler and written to a canvas of the same size (~64 MB for 16M pixels) and added to the storage.
|
||||
- Image Storage (addon)
|
||||
The image storage implements a FIFO cache, that will remove old images, if a new one arrives and `storageLimit` is hit (default 128 MB). The storage holds a canvas with the original image, and may additionally hold resized versions of images after a font rescaling. Both are accounted in `storageUsage` as a rough estimation of _pixels x 4 channels_.
|
||||
|
||||
Following the steps above, a rough estimation of maximum memory usage by the addon can be calculated with these formulas (in bytes):
|
||||
```typescript
|
||||
// storage alone
|
||||
const storageBytes = storageUsage * storageLimit * 1024 * 1024;
|
||||
// decoding alone
|
||||
const decodingBytes = sixelSizeLimit + 2 * (pixelLimit * 4);
|
||||
|
||||
// totals
|
||||
// inactive decoding
|
||||
const totalInactive = storageBytes;
|
||||
// active decoding
|
||||
const totalActive = storageBytes + decodingBytes;
|
||||
```
|
||||
|
||||
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
|
||||
Further note that the formulas above do not respect the Javascript object's overhead. Compared to the raw buffer needs the book keeping by these objects is rather small (<<5%).
|
||||
|
||||
_Why should I care about memory usage at all?_
|
||||
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the Javascript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
|
||||
|
||||
_How can I adjust the memory usage?_
|
||||
- `pixelLimit`
|
||||
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
|
||||
- `storageLimit`
|
||||
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
|
||||
- `sixelSizeLimit`
|
||||
A constructor setting. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken SIXEL sequences at an earlier phase, thus may stop the decoder from entering its memory intensive task for potentially invalid data.
|
||||
|
||||
|
||||
### Terminal Interaction
|
||||
|
||||
- Images already on the terminal screen will reshape on font-rescaling to keep the terminal cell coverage intact.
|
||||
This behavior might diverge from other terminals, but is in general the more desired one.
|
||||
- On terminal resize images may expand to the right automatically, if they were right-truncated before.
|
||||
They never expand to the bottom, if they were bottom-truncated before (e.g. from scrolling-off).
|
||||
- Text autowrapping from terminal resize may break and wrap images into multiple parts. This is unfortunate,
|
||||
but cannot be avoided, while sticking to the stronger terminal text-grid mechanics.
|
||||
(Yes images are a second class citizen on a mainly text-driven interface.)
|
||||
- Characters written over an image will erase the image information for affected cells.
|
||||
- Images are painted above BG/FG data not erasing it. More advanced "composition tricks" like painting images
|
||||
below FG/BG data are not possible. (We currently dont hook into BG/FG rendering itself.)
|
||||
- Previous image data at a cell will be erased on new image data. (We currently have no transparency composition.)
|
||||
|
||||
|
||||
### Performance & Latency
|
||||
|
||||
- Performance should be good enough for occasional SIXEL output from REPLs, up to downscaled movies
|
||||
from `mpv` with its SIXEL renderer (tested in the demo). For 3rd party xterm.js integrations this
|
||||
furthermore depends highly on the overall incoming data throughput.
|
||||
- Image processing has a high latency. Most of the latency though is inherited from xterm.js' incoming data route
|
||||
(PTY -> server process -> websocket -> xterm.js async parsing), where every step creates more waiting time.
|
||||
Since we cannot do much about that "long line", keep that in mind when you try to run more demanding applications with realtime drawing and interactive response needs.
|
||||
|
||||
|
||||
### Status
|
||||
|
||||
- Sixel support and image handling in xterm.js is considered beta quality.
|
||||
- IIP support is in alpha stage. Please file a bug for any awkwardities.
|
||||
|
||||
|
||||
### Changelog
|
||||
|
||||
- 0.5.0 integrate with xtermjs base repo (at v0.4.3)
|
||||
- 0.4.3 defer canvas creation
|
||||
- 0.4.2 fix image canvas resize
|
||||
- 0.4.1 compat release for xterm.js 5.2.0
|
||||
- 0.4.0 IIP support
|
||||
- 0.3.1 compat release for xterm.js 5.1.0
|
||||
- 0.3.0 important change: worker removed from addon
|
||||
- 0.2.0 compat release for xterm.js 5.0.0
|
||||
- 0.1.3 bugfix: avoid striping
|
||||
- 0.1.2 bugfix: reset clear flag
|
||||
- 0.1.1 bugfixes:
|
||||
- clear sticky image tiles on render
|
||||
- create xterm-addon-image folder from bootstrap.sh
|
||||
- fix peer dependency in package.json
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash
|
||||
|
||||
# sixel endless mode
|
||||
# Should print an endless sine curve, abort with Ctrl-C.
|
||||
|
||||
period=200
|
||||
amplitude=50
|
||||
|
||||
sixels=(@\$ A\$ C\$ G\$ O\$ _\$-)
|
||||
pi=$(echo "scale=10; 4*a(1)" | bc -l)
|
||||
run=true
|
||||
trap run=false INT
|
||||
|
||||
echo -ne "\x1bP0;0;0q\"1;1#1;2;100;0;0#1"
|
||||
y=0
|
||||
while $run
|
||||
do
|
||||
x=$(echo "s(2*${pi}*${y}/${period})*${amplitude}+2*${amplitude}+0.5" | bc -l)
|
||||
echo -ne "!${x%%.*}?${sixels[$((y%6))]}"
|
||||
(( y++ ))
|
||||
done
|
||||
echo -e "\x1b\\"
|
||||
|
After Width: | Height: | Size: 76 KiB |
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# GLF should move the text cursor downwards,
|
||||
# even if no pixels were modified,
|
||||
# when sixel scrolling is on.
|
||||
|
||||
echo -n $'\e[?80l' # Ensure sixel scrolling is on (disable DECSDM)
|
||||
|
||||
clear
|
||||
echo "A test of Sixel GLF (Graphics Line Feed) when sixel scrolling is on"
|
||||
echo
|
||||
|
||||
# Move cursor down three using GLF ("-")
|
||||
echo -e '\x1bPq$-$-$-$-\x1b\\'
|
||||
|
||||
# Show a single sixel line that says, "Your terminal ->"
|
||||
cat <<'EOF'
|
||||
P0;0;0q"1;1;244;21#0;2;0;0;0#1;2;80;80;80#0~~~NFFFN~~~n!4FN!68~NFN!64~bbb!42~rbbbBB!41~$#1???owwwo???O!4wo!68?owo!64?[[[!42?K[[[{{-#0!5~{woBFB`o{}!4~NFBpp!4xpbBF~~~x@@@~~~xxp@@!5~xxp@@b`pxwxpz!17~zpp???!5px!5~NFBpxxwxxpBBN!4~xxp@@b`pxwxpz~x@@@xxp@@pxx@@F!4~xppp@@!8~x@@@pxxwxp@B!5~zxXXXWXX@BF!10~??!24~!10^FNN^~~~$#1!5?BFN{w{]NB@!4?ow{MM!4EM[{w???E}}}???EEM}}!5?EEM}}[]MEFEMC!17?CMM~~~!5ME!5?ow{MEEFEEM{{o!4?EEM}}[]MEFEMC?E}}}EEM}}MEE}}w!4?EMMM}}!8?E}}}MEEFEM}{!5?CEeeefee}{w!10?~~!24?!10_woo_-#0!5~nFFF??FFN!5~wo_FF!4NFb_o!4~{o_!4Ffb??Fn~~~NFF??!4FN!23~_??!5NFFn~~~w__C!7KCC!4~NFF??!4FN!4~N???N~~??Fn~???N~~N!4F??!4Fn~~~F???Fn~~nF??Fn~~p??KMMMEE???Fn~~~N!4F??!4Fn!29~{{}!4~$#1!5?Owww~~wwo!5?FN^ww!4ow[^N!4?BN^!4wW[~~wO???oww~~!4wo!23?^~~!5owwO???F^^z!7rzz!4?oww~~!4wo!4?o~~~o??~~wO?~~~o??o!4w~~!4wO???w~~~wO??Ow~~wO??M~~rpppxx~~~wO???o!4w~~!4wO!29?BB@-#0!25FE!63FE!15FE!77FE!60F$#1!25?@!63?@!15?@!77?@-\
|
||||
EOF
|
||||
|
||||
sleep 1
|
||||
tput cup 2 30
|
||||
|
||||
# Show four lines saying, "<- A genuine VT340 would end here"
|
||||
cat <<'EOF'
|
||||
P0;0;0q"1;1;144;80#0;2;0;0;0#1;2;80;80;80#0~~rpp@@`@BN!25~!11^!5~^^^N^^^!6~!7^N^^^!5~!4^~~~!5^!6~^^^WWW!8~!7^N^^^!8~^^^N^^^!5~$#1??KMM}}]}{o!25?!11_!5?___o___!6?!7_o___!5?!4_???!5_!6?___fff!8?!7_o___!8?___o___-#0~~NB??EFE??@F^!19~B??{!4}{W??}}~~B@?K!5MK??B~~~}???{!4}{??!4~}???~~~}}{??!6~}{{{??!8~}???{!4}{??!4~B@?K!5MK??B~~$#1??o{~~xwx~~}w_!19?{~~B!4@Bf~~@@??{}~r!5pr~~{???@~~~B!4@B~~!4?@~~~???@@B~~!6?@BBB~~!8?@~~~B!4@B~~!4?{}~r!5pr~~{-#0rpoopr~~~zpooopr!17~}{ww!4pwW??!4~}wwprrb!4rpp~~~pooopz~~zpoopz!4~{w!4pxwoopz~~~r!4poo!4pz~~~pooopz~~zpoopz~~}wwprrb!4rpp~~$#1KMNNMK???CMNNNMK!17?@BFF!4MFf~~!4?@FFMKK[!4KMM???MNNNMC??CMNNMC!4?BF!4MEFNNMC???K!4MNN!4MC???MNNNMC??CMNNMC??@FFMKK[!4KMM-#0!32~^NNNKWwww[[MNNN^~!13N!5~NN!5FNN^!10~N!4F!8~NN!4FN^!36~$#1!32?_ooorfFFFbbpooo_?!13o!5?oo!5woo_!10?o!4w!8?oo!4wo_-#0!32~}{o?AM!4~E?_w}}~o_o}}???}}}__!5~}}~fFFFAOw!6~^FB_w{???!6~B??}!4~{??!35~$#1!32?@BN~|p!4?x~^F@@?N^N@@~~~@@@^^!5?@@?Wwww|nF!6?_w{^FB~~~!6?{~~@!4?B~~-#0!36~{_?FB?o}!7~^NN???NN^!6~!8NE?_x!4~poooP@@???X!5~{o?F!4NB_o!35~$#1!36?B^~w{~N@!7?_oo~~~oo_!6?!8ox~^E!4?MNNNm}}~~~e!5?BN~w!4o{^N-#0!38~}}}!11~!8}!8~!6}!13~!6}!9~!4}!38~$#1!38?@@@!11?!8@!8?!6@!13?!6@!9?!4@-#0!5^!5~!5^!5~!7^!6~!4^~~~!5^!6~{www??!11~^^^N^\W???!22~^^^N^^^!6~!7^N^^^!8~^^^N^\W???~~$#1!5_!5?!5_!5?!7_!6?!4_???!5_!6?BFFF~~!11?___o_af~~~!22?___o___!6?!7_o___!8?___o_af~~~-#0}{??CMB@@FM??o{~~B@?{{!4}{w?@~~~}???~~~}}{??!10~??!8~B??{!5}{???!19~B@?K!5MK??B~~~}???{!4}{??!4~B??{!5}{???~~$#1@B~~zp{}}wp~~NB??{}~BB!4@BF~}???@~~~???@@B~~!10?~~!8?{~~B!5@B~~~!19?{}~r!5pr~~{???@~~~B!4@B~~!4?{~~B!5@B~~~-#0~~~woow~}woo{!4~}{wpprbrrpww{!5~{w!4pxwoopz~~~r!4poo!4pz~~~}{ww!5pwooopz!17~}wwprrb!4rpp~~~pooopz~~zpoopz~~}{ww!5pwooopz$#1???FNNF?@FNNB!4?@BFMMK[KKMFFB!5?BF!4MEFNNMC???K!4MNN!4MC???@BFF!5MFNNNMC!17?@FFMKK[!4KMM???MNNNMC??CMNNMC??@BFF!5MFNNNMC-#0!40~n!4F!99~$#1!40?O!4w-#0!10~^^n!29~???b!4rbBF!4~^NFbrrprrbFF^!4~rrbBBFBbrrrbv~~^NFbrrprrbFF^!42~$#1!10?__O!29?~~~[!4K[{w!4?_ow[KKMKK[ww_!4?KK[{{w{[KKK[G??_ow[KKMKK[ww_-#0!9~}{wq!10}!18~N???N^~~^N??N^~~o??H!6XWGG!4~^NN??!4N^!5~o??H!6XWGG!42~$#1!9?@BFL!10@!18?o~~~o_??_o~~o_??N~~u!6efvv!4?_oo~~!4o_!5?N~~u!6efvv-#0!41B!6ABB!6A!5BAAA?!6A!4B!10A!8BAAA?!6A!42B$#1!41?!6@??!6@!5?@@@B!6@!4?!10@!8?@@@B!6@$-\This text should be indented.
|
||||
EOF
|
||||
|
||||
# For more details, please see:
|
||||
# https://github.com/jerch/xterm-addon-image/issues/37
|
||||
|
||||
# By default, sixel is in a 2:1 aspect ratio, which means
|
||||
# every sixel graphics linefeed (GLF) adds another 12 pixels.
|
||||
# With four GLFs, we have five sixel lines = 5 * 12 = 60 pixels.
|
||||
|
||||
# Which text line that ends up on depends upon the height of your
|
||||
# font. On the VT340, the font is 20 pixels high.
|
||||
|
||||
# On a VT340 sixel scrolling is on by default. DEC refers to this by
|
||||
# two different names "sixel scrolling" and its negation, "DECSDM"
|
||||
# (Sixel Display Mode). They control the same thing, so when DECSDM is
|
||||
# on, sixel scrolling is off, and vice versa.
|
||||
|
||||
# When DECSDM is on, the graphic line feeds do not affect the text cursor.
|
||||
|
||||
|
||||
# How hackerb9 created the two sixel test images:
|
||||
#
|
||||
# convert -family "Courier" -style normal -density 72 -pointsize 26 -interline-spacing -12 -gravity center -fill gray80 -background none label:$'A genuine\nVT340\nwould end\n← here ' +trim +dither -colors 2 sixel:- > reference.six
|
||||
#
|
||||
# convert -family "Courier" -style normal -density 72 -pointsize 26 -interline-spacing -12 -gravity center -fill gray80 -background none label:$'Your terminal →' +trim -bordercolor none -border 2 +dither -colors 2 sixel:- > yourterminal.six
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
const sixelEncode = require('../node_modules/sixel/lib/SixelEncoder').image2sixel;
|
||||
const toRGBA8888 = require('../node_modules/sixel/lib/Colors').toRGBA8888;
|
||||
|
||||
function createRect(size, color) {
|
||||
const pixels = new Uint32Array(size * size);
|
||||
pixels.fill(toRGBA8888(...color));
|
||||
return sixelEncode(new Uint8ClampedArray(pixels.buffer), size, size);
|
||||
}
|
||||
|
||||
function createRectMinusOne(size, color) {
|
||||
const pixels = new Uint32Array(size * size);
|
||||
if (size - 1) {
|
||||
const sub = new Uint32Array(size - 1);
|
||||
sub.fill(toRGBA8888(...color));
|
||||
const last = size * (size - 1);
|
||||
for (let y = 0; y < last; y += size) {
|
||||
pixels.set(sub, y);
|
||||
}
|
||||
}
|
||||
return sixelEncode(new Uint8ClampedArray(pixels.buffer), size, size);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// clear + cursor and sixelScrolling off
|
||||
process.stdout.write('\x1b[2J\x1b[?25;80h');
|
||||
|
||||
for (let i = 1; i < 300; ++i) {
|
||||
await new Promise(res => setTimeout(() => {
|
||||
process.stdout.write(createRect(i, [0, 255, 0]));
|
||||
res();
|
||||
}, 5));
|
||||
}
|
||||
for (let i = 299; i >= 1; --i) {
|
||||
await new Promise(res => setTimeout(() => {
|
||||
process.stdout.write(createRectMinusOne(i, [0, 255, 0]));
|
||||
res();
|
||||
}, 5));
|
||||
}
|
||||
|
||||
// re-enable cursor and sixel scrolling
|
||||
process.stdout.write('\x1b[2J\x1b[?25;80l');
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1 @@
|
||||
]1337;File=inline=1;size=525;name=Li4vcGFsZXR0ZS5wbmc=:iVBORw0KGgoAAAANSUhEUgAAAoAAAABQCAYAAACJbMQlAAAABmJLR0QA/wD/AP+gvaeTAAABwklEQVR4nO3dwQnEMAwAQedIIem/yVwLBj0C3pm3uAO/Fj2ia631rh3P1lRu7vnof0+Z836zOe83m/N+sznvN5vzfrM57zeb+23+HAAAhxCAAAAxAhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIi59z8tDQDACWwAAQBiBCAAQIwABACIEYAAADECEAAgRgACAMQIQACAGAEIABAjAAEAYlwCAQCIsQEEAIgRgAAAMQIQACBGAAIAxAhAAIAYAQgAECMAAQBiBCAAQIwABACIcQkEACDGBhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIgRgAAAMQIQACDGJRAAgBgbQACAGAEIABAjAAEAYgQgAECMAAQAiBGAAAAxAhAAIEYAAgDECEAAgBiXQAAAYmwAAQBiBCAAQIwABACIEYAAADECEAAgRgACAMQIQACAGAEIABAjAAEAYlwCAQCIsQEEAIgRgAAAMQIQACBGAAIAxAhAAIAYAQgAECMAAQBiBCAAQIwABACIcQkEACDGBhAAIEYAAgDECEAAgBgBCAAQIwABAGIEIABAjAAEAIgRgAAAMQIQACDmDz4lC02AaqMRAAAAAElFTkSuQmCC
|
||||
@@ -0,0 +1 @@
|
||||
]1337;File=inline=1;size=1667;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYuZ2lm:R0lGODlhSAAwAPcAAAQCBIyKjMzOzERGRHSOvAxCnLy6vNzm9DRmrCQiJGxubNze3HSWxJSq1JyenCRarPz+/FR+vBweHLzG5FxeXGSKxBROpOzu7ERytDw+PHx+fLTC5AwODIyOjExOTAxGnLy+vDxqtCwuLHR2dNTa7ISezKS23KyurMTO5GyKxOzy/ACCXwAAN7AAF+IAjIEAw8DPArtsAOSupIHW2HBvAsUBAHIAbwAAARwAiEdgA/caAr+CAgCIALAjAOMcJIGCAAB8EADGAAByiAAArcQcSrtHBOT3zYG/AaQA7sVgE3Ib9wCCv88AZ2wAAaIAANUAAG+M4wEjogAc9wCCv8SwGbvGBeRyAIEAAF/PrBZsxwCucgDWAIRvpMUBaHIAAQAAAFCMfEIjj/kcUb+CAAC4sLAZKOL3AIG/AECQoAfGowBy9wAAvwBQAABCAAD5UAC/ALgALBlguPcaUb+CAMRwpNUFaAAAAQAAAOAAAPgAAHIAAAAAAG24DBgZAPf3UL+/AM/QAGzWAKIAUNUAAADgfAD4jwByUQAAAKJtQNUYAH/3AFe/AGTPABNsAACuAADWAAAAMgAABUYAAAIAAACuGQDWBQh/AABXAAJH1AABx8QAcgsAAAMAUAAApTdG9wYCv3oAADMAAKUBULAAAEcCZwEApQCM9wDDv0ACAAcAAABfUAAWAKUAQ7AAAEelAAGwAPhHQ8UBAHIAAAAAABhwFIkFxX8AUIEAABClAtKwADxHAIIBABAUopPXKKZm99Iov/gf+MUXx3IAcgAAAAxwZsYFtHIA9wAAvxgAAIlgAH8aUIGCAHiM1QrDtD0C94IAvxAAAJMASqYChdIAgQxw78YFGXIA+AAAvw4AM6EAGvcA+L8AvyYAALMASveNhb+tgQBKLGAEuDzNUYIBAADRmwABbgAA9wAAv8+kFmzYxWYCUNYAAG+k4wHYKAACQgAAAAAAHmAAxTsoUIIAADwcvcZ6bnIC9wAAvxgAm4kAbn8A94EAv0QABAoAAD0MAIIXACH5BAAAAAAALAAAAABIADAABwj/AAEIHEiwoMGDCBMqXGgQgsOHECNKnEixosWLGDNq3Mixo8ePIEOKHCkxgMkRBkiq7BhAgEmXAlbKxBgAwoALAyDUnMlzYs0EA1LuVIGiKAoVEg8YRXFxQoOnJpZKLYpUY0wII2wKGLBg54EPYD+YkEgi7AemFS2AVRuW7Qe3aB8K8ACAA4UFDy+IcGhg54URFx4+CFthIlsLDCqiKGD2bWOzcSE4MOjAoQYAck0GCPyQweGJFcI+qMiA8eO2azFAFIDQwAUOAEBcRNGWhEQTZg9QRFDg8NrTkSUglKBAYOWLbhNHVGG2wcSvYVNE3GC28MPJA0VoUJCAIAfOFiOE/0UwEYPjCBMbmK0quC17CBkGHodwgcJADRlxO9YdMXTY9w6Jx1gIEZXg2AclQHSBfBLZ911GzIU1VkRufTCBRL8595AKbo0G0QkCUUCRdhuFEBZ6EJFQoXUPTWCWbQ/5B9aFEF0GAF4TCQAeRuqFVeBvH3gYo2gQ0eaYahHR5cFKZTm2AUQIPAajQ4OBpZxD5oXFH0TxzUdSlR9c+VWFFiToUJNgPemQfmCxyCUAO44ko5BsukWeQz1aYMGGg7kVwpQOZSDBTC46BqN4bxn4n0MmgoUiBD2iJhaXGfDUnENuHSCeWmNFCNaEEJRQoVmgalDpTIh+oBp1YJEXKXpsfv+wJQol1IqAbxZU5cCpMuHGlgoMrJUYdG9BEJpaBFKEQoVmLsDrSsSKBSaMYC4blpkUoVmsZT1FCVaj20Lg2bcvXnQsWDDKxtO4jbFoZGNCVsTqWxr2BEGTvtGIqaRuZmsWtvaO+sF7Moalr0VuAdxTwR88uuaBYAH43G8pqItRnBkVGla9DnmqlsOK/btkRhnguFGEbG35UJZg1TtBBHdGFCxbKHSA30UBAHCCRywnG1GksjrkrYXLgTnwAgB4KZEAsN3MUaRXQkSszzJaAGiwJzpUnNJywZa0R9BZEBlED6glpltlokDCuW+h9RoAO0EUgNd7fdQnaWBFpqjAWT9gBCIAxJ1wggLCCcSByR0F22+R4a7cWIUIAIjdQRxc9RFtoBq2uAopnPZBCBJL5jVBElgOUq4WVZB5kZt+u3pECygggkAZdICxvbjnrvvuvPfu++/ABy88Q8QXb/zxAgUEADs=
|
||||
@@ -0,0 +1 @@
|
||||
]1337;File=inline=1;size=2159;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYuanBn:/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAwAEgDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwD33/hXPgn/AKE7w3/4K4P/AImj/hXPgn/oTvDf/grg/wDia8y/aLuNX0PWtB1nS7+8ghcGJ4o5mWPejbhlQcHIYj6LXr07J4l8IO1jM8K6lZboZUbayb0yrAjoRkH8K6J4flpwqX0l+BhGrzSlC2qM7/hXPgn/AKE7w3/4K4P/AImj/hXPgn/oTvDf/grg/wDia81/Zt8S313da3ousXk9xcR7biLz5S7DB2OMknvsrov2ifEEujeB0trSd4bq/uFiDRsVYIvzMQR9FH/AquWDlHELD3+ZMcQnS9qdR/wrnwT/ANCd4b/8FcH/AMTR/wAK58E/9Cd4b/8ABXB/8TXhFr4F8dL4SXX5fFCWOntbfays1/OrqhGRkKpGSMcZzyB1q7+z9D4g8QeKzqF3q2pSaZpwzIsty7LJIwIVME4Pcn0wPUVvPL4xhKaqJqJnHFNyUXFq57V/wrnwT/0J3hv/AMFcH/xNH/CufBP/AEJ3hv8A8FcH/wATXgnxP8YePPiB8Sr/AMI/DKe8tbLSG8u4ntZ/ILSKcOzy5BVQ2VCg87ScHoF0/wAcfE3T9Z8O/D7xJZSNr0mq20635YYuLJG3upZeGHycsOdoYEZ6+Ydh6Pd2fgW38Qvpn/CA+G2EaNKzfYoPMEStIrPs8voDDJ37D+JlUld9ceFNNnvzcyISGfzGjKoQW+Yn5iu8Al3yoYA73yPmbJQBzvx30X+2fhtqRRN01jtvE9tmdx/74L1ynw18Z/Y/gVqN20n+laMktuhJ5y3MX4ZdV/CvZ7qCO6tpredQ8UqGN1PdSMEfrXxPqb6hoD674WDExveKkygHLmJnC4Hvuz74Fexl8FiaToy6NP5dTz8VJ0Zqouqa/wAjb+Ed/P4c8f8Ah++uEeO0v2MIc8K6Oxjzn0DgH/gNdt8bpH8V/FnQ/DFu5KQeXC+P4GlIZz+CbD+FT/Grwf8A2L8NvCk1uu2fSQtvMyccuNzN/wB/F/8AH6p/AeO48V/E/VfE2ohWkgRpiQOFkk+VQPYLvH4V3SqQmnjV0TXzvoc6jKP+zPq0/wDM0P2k/FRt47Twnp+Y4QizXOOAQPuR/QY3H/gNdx8BtQ8PS+CoLDQJibm3G+9jlULIZW6uR3BxgEE8ADqK7nUdG0zUlcajp1pdb12t50KuSPqRXzl4bgTwP+0C2m2EhSwMxgKs/HlSR7wpP+ySp5/u1wUnDE4Z0Y6OKv6nTNSpVlUeqenoej/s26IuneAZdSmX/iY6xf3N3cuw+YkSsgB+gXOOxY16bNY2s17b3k1vE91bh1hlZQWjDY3BT2zgZ+lQ6JpcGj2H2O0GIfOllA9PMkaQgewLEVeJx1ryDvFooooAK8K8X+C/tf7QWjTCPNneKt9KSPl3QjDD8dsf/fdel/8ACxvBP/Q4+G//AAaQf/FVG3j/AMCNcJO3i3wwZ0VkSQ6lBuVWILAHdkA7VyO+B6V0YevKg249U0ZVaSqJJ9Hcu/EDRf8AhIfBesaYF3yT27eUP+mi/Mn/AI8BXm3wa8Panb/CDUbjRZ/sms6mzy28xUEgJ8qDDDAztbntuzXoH/CxvBP/AEOPhv8A8GkH/wAVUdv4/wDAltCkNv4t8MRRIMKialAqj6ANTp4mUKTpLun939IUqKlPn8rHkXg340XnhuwuNJ8Y2N9eX9s77ZWbEuSSdkm70PQjtgY4ql8KdCu/iD4+1LxNrVsTppMpkyPkd3QoI1Pfarfhgetet6n4l+F+qzCbVNa8F3soG0PcXVrIwHpliavW3j7wHawJBa+LPC8MKDCRx6jAqqPQANgV1SxtNRl7KFpS3/4BjHDTuueV0tjg0+LX/CvL5/DfxPivRNCT9i1iKHzI7+AfddgORJjAYAEZ54yM8xd/ETV/jN4v0zw/4Btrux8PWV3DealqU67WZY3DKMAkAZXhc5YgdADXrereMPhxrNr9m1fxF4Qv7bOfKur22lTPrhmIpdK8ZfDnSLRbXSfEfhGxtV5ENtfW0SD/AICrAV5h2HbUVyv/AAsbwT/0OPhv/wAGkH/xVFAH/9k=
|
||||
@@ -0,0 +1 @@
|
||||
]1337;File=inline=1;size=1658;name=Li4vdGVzdGltYWdlcy93M2NfaG9tZV8yNTYucG5n:iVBORw0KGgoAAAANSUhEUgAAAEgAAAAwCAMAAACFQszZAAADAFBMVEUEAgSMiozMzsxERkR0jrwMQpy8urzc5vQ0ZqwkIiRsbmzc3tx0lsSUqtScnpwkWqz8/vxUfrwcHhy8xuRcXlxkisQUTqTs7uxEcrQ8Pjx8fny0wuQMDgyMjoxMTkwMRpy8vrw8arQsLix0dnTU2uyEnsykttysrqzEzuRsisTs8vwAgl8AADewABfiAACBAAlwzwPVbADkrqSB1thwbwLFAQByAG8AAAEcADBHYAf3GgK/ggIA/ACwaADjHByBggAAfA4AxgAAchQAACB0HEbVRwTk98WBvwGkAO7FYBNyG/cAgr/PAGdsAAGiAADVAABvAOMBaaIAHPcAgr90sBXVxgXkcgCBAABfz6wWbMcArnIA1gCEb1TFARNyAAAAAABQANRCaU35HFG/ggAAuLCwGSji9wCBvwD4kKAIxqMAcvcAAL8AUAAAQgAA+VAAvwC4AIQZYHb3GlG/ggDElFTVDxMAAAAAAADgAAD4AAByAAAAAABtuAwYGQD391C/vwDP0ABs1gCiAFDVAAAA4NQA+E0AclEAAACibUDVGAB/9wBXvwBkzwATbAAArgAA1gAAABgAAAVGAAACAAAArhUA1gUIfwAAVwADR9QAAcd0AHIlAAADAFAAAKU3RvcGAr96AAAzAAClAVCwAABHA2cBAKUAAPcACb/4AwAIAAAAX1AAFgClAEOwAABHpQABsAD4R0PFAQByAAAAAAAYlByJD8V/AFCBAAAQpQLSsAA8RwCCAQAQFKKT1yimZvfSKL/4H/jFF8dyAHIAAAAMlGbGD7RyAPcAAL8YAACJYAB/GlCBggB4ANUKCbQ9A/eCAL8QAACTAEqmA4XSAIEMlO/GDxlyAPgAAL8OADOhABr3APi/AL8mAACzAEr3GoW/IIEARoRgBHY8xVGCAQAAz5sAAW4AAPcAAL/PpB5s2MVmAlDWAABvpOMB2CgAAkIAAAAAABZgAMU7KFCCAAA8HL3Gem5yAvcAAL8YAJuJAG5/APeBAL9EAAQKAAA9DACCFwCk+0jYAAAACXBIWXMAAAAAAAAAAACdYiYyAAADIElEQVR4nO2WW3OqQAyAAUehArJWXcDSUiwg3f//A8/msheU0/bBhzNnutMOEZKPJJtkCYJHLfGg9V+CwjD82DwAFEah/oseABKrciXCR4CeVhsEqUkvRXdjkD2tQ57n4+SWuqFEQnyIVbRKARS3eo30oAfZI8m2lXBL8j89it6C5phqobwIsQn19aOE0AqtcfIsM8uZ1q3B0ELQjkp5J8QZzHWywxJ3LQNrNj1p7cKCsrVFAE1W6I9pik3ZBO9GFUATqPX0ewSb2DxN1hSb9B16NqDnLbrlQBBPy/Eo0M/5IWbvE8WX1sSPgV3O2ye4NuUMtNdKCd+o9Ov3LOdgTRsFeZQo1pQeUR61cBYz0AjOxy5JbKxfsG47lAaIbgCpDGw8R88hAimvADAdB5KBj2EqaffgGgRHY3xxDnGvdVqP4umlTYY4gNhbN4l+DoLUGEflLQiTwTFAEIWNEqUJblZk8Ba8iaVFoB40X0BKaJ/Rj8JsZuXVRO12fAFkbWLuiEFwuwB9dNECqPwKZKIYuSESildCwasCb3VUsvXzIseADmDfY0XJgQvA7EBuOgT3ta6/BAkuaHh1vNdWo6uJwfYs/Dp/A4LirrAREnRh77puGoYhwYaD0t59AxrBd5XhEIEWkzptkssa1oRu6T1IvwHRdCto5+GCloNTxD2E3J+XMN5xBAXUsS4MqM6WEy/wEG6830NmoIxTCvUycXILXxPyJ/M7wB2o5wmGHcUj+uRr9u081r+BBHukKIzWGwKewk9AZEwzYKROmR0+MTj5eZOhm3mEC4cGT1mFoe1nNpi44ab1aztSHEhhWrjHKwM97M0MzuDx9Drb/TC43oPQ2FRgzmWd2EypAoNN/SkSNW5oe6DcnSVYnwCFxNFJlXEGt46kOTfHkTWW9rQuJM0nLIRh6rEc4bE+FEOOS3MuYgGkO0NaOePTcPAPbMz+FU7G63UL52STLoIyrwInA63MNNJzgcphZz9km0gsgiZzJMGSDFWfxp/OlNWu4SPb48w//aRXgScLnWDQtZ33knR7CYL6dTa8/6Wv2l/Qj0GPWn8AP2jx/kucqREAAAAASUVORK5CYII=
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
function print_palette() {
|
||||
L=$(( LOWER / 256))
|
||||
U=$(( (UPPER-1) / 256))
|
||||
for ((p = $L; p <= $U; p++))
|
||||
do
|
||||
echo "slot $((p*256))..$((p*256+255)):"
|
||||
echo -ne "\x1bP;1q"
|
||||
for i in {0..15}
|
||||
do
|
||||
a=$((i * 16 + p * 256))
|
||||
for j in {0..15}
|
||||
do
|
||||
echo -ne "#$((a+j))!6~"
|
||||
done
|
||||
echo -ne "\$-"
|
||||
done
|
||||
echo -e "\x1b\\"
|
||||
done
|
||||
}
|
||||
|
||||
colors=undefined
|
||||
max_colors=undefined
|
||||
|
||||
echo "Terminal Reports (XTSMGRAPHICS):"
|
||||
IFS=";" read -a REPLY -s -t 1 -d "S" -p $'\e[?1;1;0S'
|
||||
[[ ${REPLY[1]} == "0" ]] && colors=${REPLY[2]}
|
||||
echo "active colors: ${colors}"
|
||||
|
||||
IFS=";" read -a REPLY -s -t 1 -d "S" -p $'\e[?1;4;0S'
|
||||
[[ ${REPLY[1]} == "0" ]] && max_colors=${REPLY[2]}
|
||||
echo "max colors : ${max_colors}"
|
||||
echo
|
||||
|
||||
|
||||
# query up to colors by default
|
||||
# if colors is undefined (no XTSMGRAPHICS), assume 256
|
||||
ARG1=${1:-${colors}}
|
||||
if [[ $colors == "undefined" ]]
|
||||
then
|
||||
ARG1=${1:-256}
|
||||
fi
|
||||
LOWER=0
|
||||
UPPER=$ARG1
|
||||
ARG2=${2:-undefined}
|
||||
if [[ $ARG2 != "undefined" ]]
|
||||
then
|
||||
LOWER=ARG1
|
||||
UPPER=ARG2
|
||||
fi
|
||||
|
||||
if [[ $colors != "undefined" ]]
|
||||
then
|
||||
if [[ $colors -lt $UPPER ]] || [[ $colors -lt 256 ]]
|
||||
then
|
||||
echo -e "\x1b[33mNote: Active colors is smaller than test range."
|
||||
echo -e "A spec-conform terminal may repeat colors in 'slot mod ${colors}'.\x1b[m"
|
||||
echo
|
||||
fi
|
||||
else
|
||||
echo -e "\x1b[33mNote: Cannot query active colors."
|
||||
echo -e "The terminal may repeat colors beyond it max slot (e.g. slot mod 16).\x1b[m"
|
||||
echo
|
||||
fi
|
||||
|
||||
print_palette
|
||||
@@ -0,0 +1,141 @@
|
||||
#!/bin/bash
|
||||
|
||||
function smiling_smiley() {
|
||||
echo -ne '\x1bP;2q"1;1;60;60
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!4]!22?!4]$-
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!4~!22?!4~$-
|
||||
#6!60~$
|
||||
!15?#1!30N$-
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function indifferent_smiley() {
|
||||
echo -ne '\x1bP;2q"1;1;60;60
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!4]!22?!4]$-
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!30N$-
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function sad_smiley() {
|
||||
echo -ne '\x1bP;2q"1;1;60;60
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!4]!22?!4]$-
|
||||
#6!60~$-
|
||||
!60~$-
|
||||
!60~$-
|
||||
!60~$-
|
||||
!60~$
|
||||
!15?#1!30N$
|
||||
!15?#1!4o!22?!4o$-
|
||||
#6!60~$
|
||||
!15?#1!4B!22?!4B$-
|
||||
#6!60~$-
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function smiling_smiley_slim() {
|
||||
echo -ne '\x1bP;1q"1;1;60;60
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
!15?#1!4~!22?!4~$-
|
||||
!15?#1!30N
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function indifferent_smiley_slim() {
|
||||
echo -ne '\x1bP;1q"1;1;60;60
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
!15?#6!4~!22?!4~$-
|
||||
!15?!4o!22?!4o$ !15?#1!30N$-
|
||||
!15?#6!4B!22?!4B
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function sad_smiley_slim() {
|
||||
echo -ne '\x1bP;1q"1;1;60;60
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
$-
|
||||
!15?#1!30N$
|
||||
!15?#1!4o!22?!4o$-
|
||||
!15?#1!4B!22?!4B$-
|
||||
'
|
||||
echo -ne '\x1b\\'
|
||||
}
|
||||
|
||||
function full() {
|
||||
smiling_smiley
|
||||
sleep .5
|
||||
indifferent_smiley
|
||||
sleep .5
|
||||
sad_smiley
|
||||
sleep .5
|
||||
indifferent_smiley
|
||||
sleep .5
|
||||
smiling_smiley
|
||||
}
|
||||
|
||||
function slim() {
|
||||
smiling_smiley
|
||||
sleep .5
|
||||
indifferent_smiley_slim
|
||||
sleep .5
|
||||
sad_smiley_slim
|
||||
sleep .5
|
||||
indifferent_smiley_slim
|
||||
sleep .5
|
||||
smiling_smiley_slim
|
||||
}
|
||||
|
||||
# clear screen and place cursor to 1;10
|
||||
echo -ne '\x1b[2J\x1b[10;1H'
|
||||
|
||||
# switch sixel scrolling off
|
||||
echo -ne '\x1b[?80h'
|
||||
|
||||
case "$1" in
|
||||
full ) full ;;
|
||||
slim ) slim ;;
|
||||
esac
|
||||
|
||||
# re-enable sixel scrolling
|
||||
echo -ne '\x1b[?80l'
|
||||
|
After Width: | Height: | Size: 525 B |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 1.9 MiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 6.3 MiB |
|
After Width: | Height: | Size: 599 KiB |
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 333 B |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 324 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,288 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test cursor row, column placement after sixel image is sent.
|
||||
|
||||
# After a sixel image is displayed, the text cursor is moved to the
|
||||
# row of the last sixel cursor position, but the column stays the same
|
||||
# as it was before the sixel image was sent.
|
||||
#
|
||||
# This can be thought of as sixel images always ending with an
|
||||
# implicit Graphics Carriage Return (`$`).
|
||||
|
||||
# ADDENDUM: It is not as simple as I thought. When a row of sixels
|
||||
# straddles two rows of text, the text cursor can be left on the upper row.
|
||||
# It seems up to three lines of pixels may be beneath any words printed.
|
||||
#
|
||||
# The rule for when this happens is not obvious to me, but can be seen
|
||||
# with images of height: 21, 22, 23, 24, 41, 42, 81, 82, 83, 84...
|
||||
#
|
||||
# My guess:
|
||||
# for a sixel image of height h, let a=(h-1)%6 and b=(h-1)%20,
|
||||
# then, the text will overlap the image when a>b.
|
||||
#
|
||||
# If that is the case, then the entire list of heights for which this
|
||||
# will happen on the VT340's 480 pixel high screen is:
|
||||
#
|
||||
# 21 22 23 24 41 42 81 82 83 84
|
||||
# 101 102 141 142 143 144 161 162
|
||||
# 201 202 203 204 221 222 261 262 263 264 281 282
|
||||
# 321 322 323 324 341 342 381 382 383 384
|
||||
# 401 402 441 442 443 444 461 462
|
||||
#
|
||||
# Note that there are 48 entries, so that means there's a 10% chance
|
||||
# if heights are chosen randomly from 1 to 480. However, if one were
|
||||
# to always pick heights which are a multiple of the character cell
|
||||
# height (20px), then the chances are 0% as there are no problematic
|
||||
# heights divisible by 20.
|
||||
|
||||
|
||||
# Sixel images often do *not* end with a `-` (Graphics New Line = GNL)
|
||||
# which sends the sixel cursor down 6 pixels. Any text printed next
|
||||
# will potentially overlap the last row of sixels!
|
||||
|
||||
# I am not yet positive, but I believe that, in general, applications
|
||||
# should send sixel images without a GNL but then send `^J`, a text
|
||||
# newline (NL), before displaying more text or graphics.
|
||||
|
||||
# IMPORTANT: sometimes neither a graphics nor a text newline is wanted.
|
||||
# For example, if an image is full screen, either newline would cause
|
||||
# the top line to scroll off the screen.
|
||||
|
||||
# | Text cursor column | Text cursor row
|
||||
# --------|--------------------|-------------------------------------
|
||||
# !GNL !NL| Unchanged | Overlapping last line of graphics
|
||||
# !GNL NL| Column=1 | First line immediately after graphic (usually)
|
||||
# GNL !NL| Unchanged | _Sometimes_ overlapping graphics
|
||||
# GNL NL| Column=1 | First *or* second line after graphic
|
||||
|
||||
|
||||
CSI=$'\e[' # Control Sequence Introducer
|
||||
DCS=$'\eP' # Device Control String
|
||||
ST=$'\e\\' # String Terminator
|
||||
|
||||
set_cursor_pos() {
|
||||
# Home, top left is row 1, col 1.
|
||||
local row=$1 col=$2
|
||||
echo -n ${CSI}${row}';'${col}'H'
|
||||
}
|
||||
|
||||
reset_palette() {
|
||||
# Send DECRSTS to load colors from a Color Table Report
|
||||
echo -n ${DCS}'2$p'
|
||||
|
||||
echo -n "0;2;0;0;0/" # VT color #0 is black and BG text color
|
||||
|
||||
echo -n "1;2;20;20;79/" # VT color #1 is blue
|
||||
echo -n "2;2;79;13;13/" # VT color #2 is red
|
||||
echo -n "3;2;20;79;20/" # VT color #3 is green
|
||||
|
||||
echo -n "4;2;79;20;79/" # VT color #4 is magenta
|
||||
echo -n "5;2;20;79;79/" # VT color #5 is cyan
|
||||
echo -n "6;2;79;79;20/" # VT color #6 is yellow
|
||||
|
||||
echo -n "7;2;46;46;46/" # VT color #7 is gray 50% and FG text color
|
||||
echo -n "8;2;26;26;26/" # VT color #8 is gray 25%
|
||||
|
||||
echo -n "9;2;33;33;59/" # VT color #9 is pastel blue
|
||||
echo -n "10;2;59;26;26/" # VT color #10 is pastel red
|
||||
echo -n "11;2;33;59;33/" # VT color #11 is pastel green
|
||||
|
||||
echo -n "12;2;59;33;59/" # VT color #12 is pastel magenta
|
||||
echo -n "13;2;33;59;59/" # VT color #13 is pastel cyan
|
||||
echo -n "14;2;59;59;33/" # VT color #14 is pastel yellow
|
||||
|
||||
echo -n "15;2;79;79;79" # VT color #15 is gray 75% and BOLD text color
|
||||
|
||||
echo -n ${ST} # String Terminator
|
||||
}
|
||||
|
||||
# Generate square of size w with final graphics new line removed
|
||||
square() {
|
||||
# Given a color index number and (optionally) a size, row, and column,
|
||||
# draw a square with top left corner at (row, column) and of size×size px.
|
||||
# Default size 100×100px (10cols, 5 rows)
|
||||
|
||||
local -i color=${1:-1} # Default is color index 1 (blue)
|
||||
local -i size=${2:-100} # Size in pixels (defaults to 100)
|
||||
local -i row=$3 column=$4 # If set to 0, cursor is not moved
|
||||
|
||||
if [[ row -ne 0 && column -ne 0 ]]; then
|
||||
set_cursor_pos $row $column
|
||||
fi
|
||||
|
||||
# Draw a square of the right color & size
|
||||
squaresize $color $size
|
||||
}
|
||||
|
||||
squaresize() {
|
||||
# Helper for square() that uses convert to return a sixel square of
|
||||
# the right color ($1) and size ($2).
|
||||
|
||||
# Similar to this but with variable size squares:
|
||||
# echo -n ${DCS}'0;0;0q"1;1;100;100#'${color}'!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100~-!100N'${ST}
|
||||
|
||||
|
||||
local color=${1:-1} # Default color index is 1 (blue)
|
||||
local size=${2:-100} # Default size is 100x100
|
||||
|
||||
# Get a sixel string
|
||||
local sq=$(convert -geometry ${size}x${size} xc:black sixel:-)
|
||||
|
||||
# Remove ImageMagick's extraneous Graphic New Line at end of image.
|
||||
sq=${sq%-??}$'\e\\'
|
||||
|
||||
# VT340s always used the same color register for the first sixel
|
||||
# color defined no matter what number it was assigned. That means,
|
||||
# each time we send a new sixel image, the previous one's color
|
||||
# palette gets changed. We don't want squares of all the same
|
||||
# color, so remove the color definition and just use the defaults.
|
||||
sq=${sq/\#0;2;0;0;0/}
|
||||
|
||||
# And finally, switch to the proper index for the color we want.
|
||||
echo -n ${sq//\#0/#${color}}
|
||||
}
|
||||
|
||||
squaregnl() {
|
||||
# Same as square(), but sends a graphics newline at the end of the sixels.
|
||||
# (Sticks a `-` before the String Terminator, "Esc \")
|
||||
sq=$(square "${@}")
|
||||
echo -n ${sq%??}$'-\e\\'
|
||||
}
|
||||
|
||||
|
||||
main() {
|
||||
clear
|
||||
reset_palette
|
||||
show_labels
|
||||
neither_graphic_nor_text 96 4 4 31 # size, color, row, column
|
||||
text_newline_only 96 9 4 1
|
||||
text_newline_only 84 9 4 14
|
||||
graphics_new_line_only 100 1 4 51
|
||||
graphics_new_line_only 96 1 4 64
|
||||
set_cursor_pos 1000 1
|
||||
}
|
||||
|
||||
|
||||
neither_graphic_nor_text() {
|
||||
# Typically sixel images should not end with a Graphics New Line (GNL)
|
||||
# However, if a text newline isn't sent, there will be overlap.
|
||||
|
||||
local -i size color row column
|
||||
read size color row column <<<"$@"
|
||||
|
||||
set_cursor_pos $((row++)) $column
|
||||
echo -n "Height $size"
|
||||
set_cursor_pos $((row++)) $column
|
||||
|
||||
# Three squares sent as separate sixel images, indented +1
|
||||
for i in {1..3}; do
|
||||
square $((color++)) $size
|
||||
tput cuf 1
|
||||
done
|
||||
|
||||
echo -n "overlap?"
|
||||
}
|
||||
|
||||
|
||||
text_newline_only() {
|
||||
# USING A TEXT NEWLINE (NL) after an sixel image that does NOT
|
||||
# have GNL is probably the best way to be on the text line
|
||||
# immediately below the image. However, the text will still
|
||||
# occasionally overlap the last four rows of pixels.
|
||||
|
||||
# Also, if multiple images are intended to be shown, there will
|
||||
# usually be a gap between them when using a text newline.
|
||||
|
||||
# Overlap happens because the height of a text cell is 20 pixels
|
||||
# and the height of a sixel is 6.
|
||||
# for (h=0; h<480; h++) if ((h-1)%6 > (h-1)%20 ) { h }
|
||||
|
||||
# Let the pixel position of the top of the graphics cursor be 'Yg'
|
||||
# and let the pixel position of the top of the corresponding cell
|
||||
# of text which the text cursor will be placed on be 'Yt'. Note
|
||||
# that Yg is evenly divisible by 6 and Yt, by 20. Taking the
|
||||
# remainder, r, after dividing Yg by 20 tells us how many pixels
|
||||
# down into a row of text the last line of sixels started. When
|
||||
# r==0, the sixels started at the top of the text row.
|
||||
|
||||
# When r = 14, the sixels covered the bottom six pixels on the row
|
||||
# of text. When 14 < r < 20, the sixel line impinged by r - 14
|
||||
# pixels into the text row below and there is a chance the next
|
||||
# text printed will overlap.
|
||||
|
||||
|
||||
local -i size color row column
|
||||
read size color row column <<<"$@"
|
||||
|
||||
local -i offset
|
||||
offset=$((column-1))
|
||||
|
||||
set_cursor_pos $row 1
|
||||
|
||||
if ((offset)); then tput cuf $((offset)); fi
|
||||
echo "Height $size"
|
||||
|
||||
# Three squares, separated by text new lines and indented +1
|
||||
for i in {1..3}; do
|
||||
if ((offset)); then tput cuf $offset; fi
|
||||
square $((color++)) $size
|
||||
offset=offset+1
|
||||
echo
|
||||
done
|
||||
|
||||
tput cuf $((offset))
|
||||
echo -n "overlap?"
|
||||
}
|
||||
|
||||
graphics_new_line_only() {
|
||||
# However, some sixel images end with a `-`, a Graphics New Line.
|
||||
# This can be useful for writing another image starting at the same
|
||||
# column without having to reposition the cursor.
|
||||
#
|
||||
# However, this runs the risk of having occasional overlap.
|
||||
|
||||
local -i size color row column
|
||||
read size color row column <<<"$@"
|
||||
|
||||
set_cursor_pos $((row++)) $column
|
||||
echo -n "Height $size"
|
||||
set_cursor_pos $((row++)) $column
|
||||
|
||||
# Three squares, separated by graphics new lines and indented +1
|
||||
for i in {1..3}; do
|
||||
squaregnl $((color++)) $size
|
||||
tput cuf 1
|
||||
done
|
||||
|
||||
echo -n "overlap?"
|
||||
}
|
||||
|
||||
|
||||
show_labels() {
|
||||
set_cursor_pos 1 10
|
||||
echo -n "Should sixel images include a GNL ('-') at the end?"
|
||||
|
||||
set_cursor_pos 3 29
|
||||
echo -n "Neither NL nor GNL"
|
||||
set_cursor_pos 3 1
|
||||
echo -n "Text New Line only"
|
||||
set_cursor_pos 3 51
|
||||
echo -n "Graphics New Line only"
|
||||
|
||||
set_cursor_pos 22 29
|
||||
echo -n "Always overlaps" # Neither NL nor GNL
|
||||
|
||||
set_cursor_pos 22 3
|
||||
echo -n "Overlaps a little" # NL only
|
||||
set_cursor_pos 23 3
|
||||
echo -n " Gaps a little" # NL only
|
||||
|
||||
set_cursor_pos 22 54
|
||||
echo -n "Overlaps badly" # GNL only
|
||||
set_cursor_pos 23 54
|
||||
echo -n "Never gaps" # GNL only
|
||||
}
|
||||
|
||||
|
||||
main
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "xterm-addon-image",
|
||||
"version": "0.6.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
},
|
||||
"main": "lib/xterm-addon-image.js",
|
||||
"types": "typings/xterm-addon-image.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"terminal",
|
||||
"image",
|
||||
"sixel",
|
||||
"xterm",
|
||||
"xterm.js"
|
||||
],
|
||||
"scripts": {
|
||||
"prepackage": "../../node_modules/.bin/tsc -p .",
|
||||
"package": "../../node_modules/.bin/webpack",
|
||||
"prepublishOnly": "npm run package"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"xterm": "^5.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"sixel": "^0.16.0",
|
||||
"xterm-wasm-parts": "^0.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { IImageAddonOptions, IOscHandler, IResetHandler, ITerminalExt } from './Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import Base64Decoder from 'xterm-wasm-parts/lib/base64/Base64Decoder.wasm';
|
||||
import { HeaderParser, IHeaderFields, HeaderState } from './IIPHeaderParser';
|
||||
import { imageType, UNSUPPORTED_TYPE } from './IIPMetrics';
|
||||
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const Buffer: any;
|
||||
|
||||
// limit hold memory in base64 decoder
|
||||
const KEEP_DATA = 4194304;
|
||||
|
||||
// default IIP header values
|
||||
const DEFAULT_HEADER: IHeaderFields = {
|
||||
name: 'Unnamed file',
|
||||
size: 0,
|
||||
width: 'auto',
|
||||
height: 'auto',
|
||||
preserveAspectRatio: 1,
|
||||
inline: 0
|
||||
};
|
||||
|
||||
|
||||
export class IIPHandler implements IOscHandler, IResetHandler {
|
||||
private _aborted = false;
|
||||
private _hp = new HeaderParser();
|
||||
private _header: IHeaderFields = DEFAULT_HEADER;
|
||||
private _dec = new Base64Decoder(KEEP_DATA);
|
||||
private _metrics = UNSUPPORTED_TYPE;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _renderer: ImageRenderer,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {}
|
||||
|
||||
public reset(): void {}
|
||||
|
||||
public start(): void {
|
||||
this._aborted = false;
|
||||
this._header = DEFAULT_HEADER;
|
||||
this._metrics = UNSUPPORTED_TYPE;
|
||||
this._hp.reset();
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted) return;
|
||||
|
||||
if (this._hp.state === HeaderState.END) {
|
||||
if (this._dec.put(data, start, end)) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
} else {
|
||||
const dataPos = this._hp.parse(data, start, end);
|
||||
if (dataPos === -1) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
if (dataPos > 0) {
|
||||
this._header = Object.assign({}, DEFAULT_HEADER, this._hp.fields);
|
||||
if (!this._header.inline || !this._header.size || this._header.size > this._opts.iipSizeLimit) {
|
||||
this._aborted = true;
|
||||
return;
|
||||
}
|
||||
this._dec.init(this._header.size);
|
||||
if (this._dec.put(data, dataPos, end)) {
|
||||
this._dec.release();
|
||||
this._aborted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public end(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted) return true;
|
||||
|
||||
let w = 0;
|
||||
let h = 0;
|
||||
|
||||
// early exit condition chain
|
||||
let cond: number | boolean = true;
|
||||
if (cond = success) {
|
||||
if (cond = !this._dec.end()) {
|
||||
this._metrics = imageType(this._dec.data8);
|
||||
if (cond = this._metrics.mime !== 'unsupported') {
|
||||
w = this._metrics.width;
|
||||
h = this._metrics.height;
|
||||
if (cond = w && h && w * h < this._opts.pixelLimit) {
|
||||
[w, h] = this._resize(w, h).map(Math.floor);
|
||||
cond = w && h && w * h < this._opts.pixelLimit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!cond) {
|
||||
this._dec.release();
|
||||
return true;
|
||||
}
|
||||
|
||||
const blob = new Blob([this._dec.data8], { type: this._metrics.mime });
|
||||
this._dec.release();
|
||||
|
||||
if (!window.createImageBitmap) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const img = new Image();
|
||||
return new Promise<boolean>(r => {
|
||||
img.addEventListener('load', () => {
|
||||
URL.revokeObjectURL(url);
|
||||
const canvas = ImageRenderer.createCanvas(window.document, w, h);
|
||||
canvas.getContext('2d')?.drawImage(img, 0, 0, w, h);
|
||||
this._storage.addImage(canvas);
|
||||
r(true);
|
||||
});
|
||||
img.src = url;
|
||||
// sanity measure to avoid terminal blocking from dangling promise
|
||||
// happens from corrupt data (onload never gets fired)
|
||||
setTimeout(() => r(true), 1000);
|
||||
});
|
||||
}
|
||||
return createImageBitmap(blob, { resizeWidth: w, resizeHeight: h })
|
||||
.then(bm => {
|
||||
this._storage.addImage(bm);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private _resize(w: number, h: number): [number, number] {
|
||||
const cw = this._renderer.dimensions?.css.cell.width || CELL_SIZE_DEFAULT.width;
|
||||
const ch = this._renderer.dimensions?.css.cell.height || CELL_SIZE_DEFAULT.height;
|
||||
const width = this._renderer.dimensions?.css.canvas.width || cw * this._coreTerminal.cols;
|
||||
const height = this._renderer.dimensions?.css.canvas.height || ch * this._coreTerminal.rows;
|
||||
|
||||
const rw = this._dim(this._header.width!, width, cw);
|
||||
const rh = this._dim(this._header.height!, height, ch);
|
||||
if (!rw && !rh) {
|
||||
const wf = width / w; // TODO: should this respect initial cursor offset?
|
||||
const hf = (height - ch) / h; // TODO: fix offset issues from float cell height
|
||||
const f = Math.min(wf, hf);
|
||||
return f < 1 ? [w * f, h * f] : [w, h];
|
||||
}
|
||||
return !rw
|
||||
? [w * rh / h, rh]
|
||||
: this._header.preserveAspectRatio || !rw || !rh
|
||||
? [rw, h * rw / w] : [rw, rh];
|
||||
}
|
||||
|
||||
private _dim(s: string, total: number, cdim: number): number {
|
||||
if (s === 'auto') return 0;
|
||||
if (s.endsWith('%')) return parseInt(s.slice(0, -1)) * total / 100;
|
||||
if (s.endsWith('px')) return parseInt(s.slice(0, -2));
|
||||
return parseInt(s) * cdim;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { HeaderParser, HeaderState, IHeaderFields } from './IIPHeaderParser';
|
||||
|
||||
|
||||
const CASES: [string, IHeaderFields][] = [
|
||||
['File=size=123456;name=dGVzdA==:', {name: 'test', size: 123456}],
|
||||
['File=size=123456;name=dGVzdA:', {name: 'test', size: 123456}],
|
||||
// utf-8 encoding in name
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw==:', {name: 'ümläutß', size: 123456}],
|
||||
['File=size=123456;name=w7xtbMOkdXTDnw:', {name: 'ümläutß', size: 123456}],
|
||||
// full header spec
|
||||
[
|
||||
'File=inline=1;width=10px;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
||||
{
|
||||
inline: 1,
|
||||
width: '10px',
|
||||
height: '20%',
|
||||
preserveAspectRatio: 1,
|
||||
size: 123456,
|
||||
name: 'ümläutß'
|
||||
}
|
||||
],
|
||||
[
|
||||
'File=inline=1;width=auto;height=20;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:',
|
||||
{
|
||||
inline: 1,
|
||||
width: 'auto',
|
||||
height: '20',
|
||||
preserveAspectRatio: 1,
|
||||
size: 123456,
|
||||
name: 'ümläutß'
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
function fromBs(bs: string): Uint32Array {
|
||||
const r = new Uint32Array(bs.length);
|
||||
for (let i = 0; i < r.length; ++i) r[i] = bs.charCodeAt(i);
|
||||
return r;
|
||||
}
|
||||
|
||||
describe('IIPHeaderParser', () => {
|
||||
it('at once', () => {
|
||||
const hp = new HeaderParser();
|
||||
for (const example of CASES) {
|
||||
hp.reset();
|
||||
const inp = fromBs(example[0]);
|
||||
const res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, example[1]);
|
||||
}
|
||||
});
|
||||
it('bytewise', () => {
|
||||
const hp = new HeaderParser();
|
||||
for (const example of CASES) {
|
||||
hp.reset();
|
||||
const inp = fromBs(example[0]);
|
||||
let pos = 0;
|
||||
let res = -2;
|
||||
while (res === -2 && pos < inp.length) {
|
||||
res = hp.parse(new Uint32Array([inp[pos++]]), 0, 1);
|
||||
}
|
||||
assert.strictEqual(res, 1);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, example[1]);
|
||||
}
|
||||
});
|
||||
it('no File= starter', () => {
|
||||
const hp = new HeaderParser();
|
||||
let inp = fromBs('size=123456;name=dGVzdA==:');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -1);
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
it('empty key - error', () => {
|
||||
const hp = new HeaderParser();
|
||||
let inp = fromBs('File=size=123456;=dGVzdA==:');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -1);
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
it('empty size value - set to 0', () => {
|
||||
const hp = new HeaderParser();
|
||||
let inp = fromBs('File=size=;name=dGVzdA==:');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, {name: 'test', size: 0});
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
it('empty name value - set to empty string', () => {
|
||||
const hp = new HeaderParser();
|
||||
let inp = fromBs('File=size=123456;name=:');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, {name: '', size: 123456});
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
it('empty size value - error', () => {
|
||||
const hp = new HeaderParser();
|
||||
let inp = fromBs('File=inline=1;width=;height=20%;preserveAspectRatio=1;size=123456;name=w7xtbMOkdXTDnw:');
|
||||
let res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, -1);
|
||||
hp.reset();
|
||||
inp = fromBs(CASES[0][0]);
|
||||
res = hp.parse(inp, 0, inp.length);
|
||||
assert.strictEqual(res, inp.length);
|
||||
assert.strictEqual(hp.state, HeaderState.END);
|
||||
assert.deepEqual(hp.fields, CASES[0][1]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const Buffer: any;
|
||||
|
||||
|
||||
export interface IHeaderFields {
|
||||
// base-64 encoded filename. Defaults to "Unnamed file".
|
||||
name: string;
|
||||
// File size in bytes. The file transfer will be canceled if this size is exceeded.
|
||||
size: number;
|
||||
/**
|
||||
* Optional width and height to render:
|
||||
* - N: N character cells.
|
||||
* - Npx: N pixels.
|
||||
* - N%: N percent of the session's width or height.
|
||||
* - auto: The image's inherent size will be used to determine an appropriate dimension.
|
||||
*/
|
||||
width?: string;
|
||||
height?: string;
|
||||
// Optional, defaults to 1 respecting aspect ratio (width takes precedence).
|
||||
preserveAspectRatio?: number;
|
||||
// Optional, defaults to 0. If set to 1, the file will be displayed inline, else downloaded
|
||||
// (download not supported).
|
||||
inline?: number;
|
||||
}
|
||||
|
||||
export const enum HeaderState {
|
||||
START = 0,
|
||||
ABORT = 1,
|
||||
KEY = 2,
|
||||
VALUE = 3,
|
||||
END = 4
|
||||
}
|
||||
|
||||
// field value decoders
|
||||
|
||||
// ASCII bytes to string
|
||||
function toStr(data: Uint32Array): string {
|
||||
let s = '';
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
s += String.fromCharCode(data[i]);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// digits to integer
|
||||
function toInt(data: Uint32Array): number {
|
||||
let v = 0;
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
if (data[i] < 48 || data[i] > 57) {
|
||||
throw new Error('illegal char');
|
||||
}
|
||||
v = v * 10 + data[i] - 48;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// check for correct size entry
|
||||
function toSize(data: Uint32Array): string {
|
||||
const v = toStr(data);
|
||||
if (!v.match(/^((auto)|(\d+?((px)|(%)){0,1}))$/)) {
|
||||
throw new Error('illegal size');
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// name is base64 encoded utf-8
|
||||
function toName(data: Uint32Array): string {
|
||||
if (typeof Buffer !== 'undefined') {
|
||||
return Buffer.from(toStr(data), 'base64').toString();
|
||||
}
|
||||
const bs = atob(toStr(data));
|
||||
const b = new Uint8Array(bs.length);
|
||||
for (let i = 0; i < b.length; ++i) {
|
||||
b[i] = bs.charCodeAt(i);
|
||||
}
|
||||
return new TextDecoder().decode(b);
|
||||
}
|
||||
|
||||
const DECODERS: {[key: string]: (v: Uint32Array) => any} = {
|
||||
inline: toInt,
|
||||
size: toInt,
|
||||
name: toName,
|
||||
width: toSize,
|
||||
height: toSize,
|
||||
preserveAspectRatio: toInt
|
||||
};
|
||||
|
||||
|
||||
const FILE_MARKER = [70, 105, 108, 101];
|
||||
const MAX_FIELDCHARS = 1024;
|
||||
|
||||
|
||||
export class HeaderParser {
|
||||
public state: HeaderState = HeaderState.START;
|
||||
private _buffer = new Uint32Array(MAX_FIELDCHARS);
|
||||
private _position = 0;
|
||||
private _key = '';
|
||||
public fields: {[key: string]: any} = {};
|
||||
|
||||
public reset(): void {
|
||||
this._buffer.fill(0);
|
||||
this.state = HeaderState.START;
|
||||
this._position = 0;
|
||||
this.fields = {};
|
||||
this._key = '';
|
||||
}
|
||||
|
||||
public parse(data: Uint32Array, start: number, end: number): number {
|
||||
let state = this.state;
|
||||
let pos = this._position;
|
||||
const buffer = this._buffer;
|
||||
if (state === HeaderState.ABORT || state === HeaderState.END) return -1;
|
||||
if (state === HeaderState.START && pos > 6) return -1;
|
||||
for (let i = start; i < end; ++i) {
|
||||
const c = data[i];
|
||||
switch (c) {
|
||||
case 59: // ;
|
||||
if (!this._storeValue(pos)) return this._a();
|
||||
state = HeaderState.KEY;
|
||||
pos = 0;
|
||||
break;
|
||||
case 61: // =
|
||||
if (state === HeaderState.START) {
|
||||
for (let k = 0; k < FILE_MARKER.length; ++k) {
|
||||
if (buffer[k] !== FILE_MARKER[k]) return this._a();
|
||||
}
|
||||
state = HeaderState.KEY;
|
||||
pos = 0;
|
||||
} else if (state === HeaderState.KEY) {
|
||||
if (!this._storeKey(pos)) return this._a();
|
||||
state = HeaderState.VALUE;
|
||||
pos = 0;
|
||||
} else if (state === HeaderState.VALUE) {
|
||||
if (pos >= MAX_FIELDCHARS) return this._a();
|
||||
buffer[pos++] = c;
|
||||
}
|
||||
break;
|
||||
case 58: // :
|
||||
if (state === HeaderState.VALUE) {
|
||||
if (!this._storeValue(pos)) return this._a();
|
||||
}
|
||||
this.state = HeaderState.END;
|
||||
return i + 1;
|
||||
default:
|
||||
if (pos >= MAX_FIELDCHARS) return this._a();
|
||||
buffer[pos++] = c;
|
||||
}
|
||||
}
|
||||
this.state = state;
|
||||
this._position = pos;
|
||||
return -2;
|
||||
}
|
||||
|
||||
private _a(): number {
|
||||
this.state = HeaderState.ABORT;
|
||||
return -1;
|
||||
}
|
||||
|
||||
private _storeKey(pos: number): boolean {
|
||||
const k = toStr(this._buffer.subarray(0, pos));
|
||||
if (k) {
|
||||
this._key = k;
|
||||
this.fields[k] = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _storeValue(pos: number): boolean {
|
||||
if (this._key) {
|
||||
try {
|
||||
const v = this._buffer.slice(0, pos);
|
||||
this.fields[this._key] = DECODERS[this._key] ? DECODERS[this._key](v) : v;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { imageType, IMetrics } from './IIPMetrics';
|
||||
|
||||
// fix missing nodejs decl
|
||||
declare const require: (s: string) => any;
|
||||
const fs = require('fs');
|
||||
|
||||
|
||||
const TEST_IMAGES: [string, IMetrics][] = [
|
||||
['w3c_home_256.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_256.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_256.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home_2.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_2.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_2.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home_animation.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_gray.gif', { mime: 'image/gif', width: 72, height: 48 }],
|
||||
['w3c_home_gray.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home_gray.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['w3c_home.jpg', { mime: 'image/jpeg', width: 72, height: 48 }],
|
||||
['w3c_home.png', { mime: 'image/png', width: 72, height: 48 }],
|
||||
['spinfox.png', { mime: 'image/png', width: 148, height: 148 }],
|
||||
['iphone_hdr_YES.jpg', { mime: 'image/jpeg', width: 3264, height: 2448 }],
|
||||
['nikon-e950.jpg', { mime: 'image/jpeg', width: 800, height: 600 }],
|
||||
['agfa-makernotes.jpg', { mime: 'image/jpeg', width: 8, height: 8 }],
|
||||
['sony-alpha-6000.jpg', { mime: 'image/jpeg', width: 6000, height: 4000 }]
|
||||
];
|
||||
|
||||
|
||||
describe('IIPMetrics', () => {
|
||||
it('bunch of testimages', () => {
|
||||
for (let i = 0; i < TEST_IMAGES.length; ++i) {
|
||||
const imageData = fs.readFileSync('./addons/xterm-addon-image/fixture/testimages/' + TEST_IMAGES[i][0]);
|
||||
assert.deepStrictEqual(imageType(imageData), TEST_IMAGES[i][1]);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
|
||||
export type ImageType = 'image/png' | 'image/jpeg' | 'image/gif' | 'unsupported' | '';
|
||||
|
||||
export interface IMetrics {
|
||||
mime: ImageType;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const UNSUPPORTED_TYPE: IMetrics = {
|
||||
mime: 'unsupported',
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
|
||||
export function imageType(d: Uint8Array): IMetrics {
|
||||
if (d.length < 24) {
|
||||
return UNSUPPORTED_TYPE;
|
||||
}
|
||||
const d32 = new Uint32Array(d.buffer, d.byteOffset, 6);
|
||||
// PNG: 89 50 4E 47 0D 0A 1A 0A (8 first bytes == magic number for PNG)
|
||||
// + first chunk must be IHDR
|
||||
if (d32[0] === 0x474E5089 && d32[1] === 0x0A1A0A0D && d32[3] === 0x52444849) {
|
||||
return {
|
||||
mime: 'image/png',
|
||||
width: d[16] << 24 | d[17] << 16 | d[18] << 8 | d[19],
|
||||
height: d[20] << 24 | d[21] << 16 | d[22] << 8 | d[23]
|
||||
};
|
||||
}
|
||||
// JPEG: FF D8 FF E0 xx xx JFIF or FF D8 FF E1 xx xx Exif 00 00
|
||||
if ((d32[0] === 0xE0FFD8FF || d32[0] === 0xE1FFD8FF)
|
||||
&& (
|
||||
(d[6] === 0x4a && d[7] === 0x46 && d[8] === 0x49 && d[9] === 0x46)
|
||||
|| (d[6] === 0x45 && d[7] === 0x78 && d[8] === 0x69 && d[9] === 0x66)
|
||||
)
|
||||
) {
|
||||
const [width, height] = jpgSize(d);
|
||||
return { mime: 'image/jpeg', width, height };
|
||||
}
|
||||
// GIF: GIF87a or GIF89a
|
||||
if (d32[0] === 0x38464947 && (d[4] === 0x37 || d[4] === 0x39) && d[5] === 0x61) {
|
||||
return {
|
||||
mime: 'image/gif',
|
||||
width: d[7] << 8 | d[6],
|
||||
height: d[9] << 8 | d[8]
|
||||
};
|
||||
}
|
||||
return UNSUPPORTED_TYPE;
|
||||
}
|
||||
|
||||
function jpgSize(d: Uint8Array): [number, number] {
|
||||
const len = d.length;
|
||||
let i = 4;
|
||||
let blockLength = d[i] << 8 | d[i + 1];
|
||||
while (true) {
|
||||
i += blockLength;
|
||||
if (i >= len) {
|
||||
// exhausted without size info
|
||||
return [0, 0];
|
||||
}
|
||||
if (d[i] !== 0xFF) {
|
||||
return [0, 0];
|
||||
}
|
||||
if (d[i + 1] === 0xC0 || d[i + 1] === 0xC2) {
|
||||
if (i + 8 < len) {
|
||||
return [
|
||||
d[i + 7] << 8 | d[i + 8],
|
||||
d[i + 5] << 8 | d[i + 6]
|
||||
];
|
||||
}
|
||||
return [0, 0];
|
||||
}
|
||||
i += 2;
|
||||
blockLength = d[i] << 8 | d[i + 1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,317 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IIPHandler } from './IIPHandler';
|
||||
import { ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ImageStorage, CELL_SIZE_DEFAULT } from './ImageStorage';
|
||||
import { SixelHandler } from './SixelHandler';
|
||||
import { ITerminalExt, IImageAddonOptions, IResetHandler } from './Types';
|
||||
|
||||
|
||||
// default values of addon ctor options
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
enableSizeReports: true,
|
||||
pixelLimit: 16777216, // limit to 4096 * 4096 pixels
|
||||
sixelSupport: true,
|
||||
sixelScrolling: true,
|
||||
sixelPaletteLimit: 256,
|
||||
sixelSizeLimit: 25000000,
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
};
|
||||
|
||||
// max palette size supported by the sixel lib (compile time setting)
|
||||
const MAX_SIXEL_PALETTE_SIZE = 4096;
|
||||
|
||||
// definitions for _xtermGraphicsAttributes sequence
|
||||
const enum GaItem {
|
||||
COLORS = 1,
|
||||
SIXEL_GEO = 2,
|
||||
REGIS_GEO = 3
|
||||
}
|
||||
const enum GaAction {
|
||||
READ = 1,
|
||||
SET_DEFAULT = 2,
|
||||
SET = 3,
|
||||
READ_MAX = 4
|
||||
}
|
||||
const enum GaStatus {
|
||||
SUCCESS = 0,
|
||||
ITEM_ERROR = 1,
|
||||
ACTION_ERROR = 2,
|
||||
FAILURE = 3
|
||||
}
|
||||
|
||||
|
||||
export class ImageAddon implements ITerminalAddon {
|
||||
private _opts: IImageAddonOptions;
|
||||
private _defaultOpts: IImageAddonOptions;
|
||||
private _storage: ImageStorage | undefined;
|
||||
private _renderer: ImageRenderer | undefined;
|
||||
private _disposables: IDisposable[] = [];
|
||||
private _terminal: ITerminalExt | undefined;
|
||||
private _handlers: Map<String, IResetHandler> = new Map();
|
||||
|
||||
constructor(opts?: Partial<IImageAddonOptions>) {
|
||||
this._opts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
||||
this._defaultOpts = Object.assign({}, DEFAULT_OPTIONS, opts);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const obj of this._disposables) {
|
||||
obj.dispose();
|
||||
}
|
||||
this._disposables.length = 0;
|
||||
this._handlers.clear();
|
||||
}
|
||||
|
||||
private _disposeLater(...args: IDisposable[]): void {
|
||||
for (const obj of args) {
|
||||
this._disposables.push(obj);
|
||||
}
|
||||
}
|
||||
|
||||
public activate(terminal: ITerminalExt): void {
|
||||
this._terminal = terminal;
|
||||
|
||||
// internal data structures
|
||||
this._renderer = new ImageRenderer(terminal);
|
||||
this._storage = new ImageStorage(terminal, this._renderer, this._opts);
|
||||
|
||||
// enable size reports
|
||||
if (this._opts.enableSizeReports) {
|
||||
// const windowOptions = terminal.getOption('windowOptions');
|
||||
// windowOptions.getWinSizePixels = true;
|
||||
// windowOptions.getCellSizePixels = true;
|
||||
// windowOptions.getWinSizeChars = true;
|
||||
// terminal.setOption('windowOptions', windowOptions);
|
||||
const windowOps = terminal.options.windowOptions || {};
|
||||
windowOps.getWinSizePixels = true;
|
||||
windowOps.getCellSizePixels = true;
|
||||
windowOps.getWinSizeChars = true;
|
||||
terminal.options.windowOptions = windowOps;
|
||||
}
|
||||
|
||||
this._disposeLater(
|
||||
this._renderer,
|
||||
this._storage,
|
||||
|
||||
// DECSET/DECRST/DA1/XTSMGRAPHICS handlers
|
||||
terminal.parser.registerCsiHandler({ prefix: '?', final: 'h' }, params => this._decset(params)),
|
||||
terminal.parser.registerCsiHandler({ prefix: '?', final: 'l' }, params => this._decrst(params)),
|
||||
terminal.parser.registerCsiHandler({ final: 'c' }, params => this._da1(params)),
|
||||
terminal.parser.registerCsiHandler({ prefix: '?', final: 'S' }, params => this._xtermGraphicsAttributes(params)),
|
||||
|
||||
// render hook
|
||||
terminal.onRender(range => this._storage?.render(range)),
|
||||
|
||||
/**
|
||||
* reset handlers covered:
|
||||
* - DECSTR
|
||||
* - RIS
|
||||
* - Terminal.reset()
|
||||
*/
|
||||
terminal.parser.registerCsiHandler({ intermediates: '!', final: 'p' }, () => this.reset()),
|
||||
terminal.parser.registerEscHandler({ final: 'c' }, () => this.reset()),
|
||||
terminal._core._inputHandler.onRequestReset(() => this.reset()),
|
||||
|
||||
// wipe canvas and delete alternate images on buffer switch
|
||||
terminal.buffer.onBufferChange(() => this._storage?.wipeAlternate()),
|
||||
|
||||
// extend images to the right on resize
|
||||
terminal.onResize(metrics => this._storage?.viewportResize(metrics))
|
||||
);
|
||||
|
||||
// SIXEL handler
|
||||
if (this._opts.sixelSupport) {
|
||||
const sixelHandler = new SixelHandler(this._opts, this._storage!, terminal);
|
||||
this._handlers.set('sixel', sixelHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerDcsHandler({ final: 'q' }, sixelHandler)
|
||||
);
|
||||
}
|
||||
|
||||
// iTerm IIP handler
|
||||
if (this._opts.iipSupport) {
|
||||
const iipHandler = new IIPHandler(this._opts, this._renderer!, this._storage!, terminal);
|
||||
this._handlers.set('iip', iipHandler);
|
||||
this._disposeLater(
|
||||
terminal._core._inputHandler._parser.registerOscHandler(1337, iipHandler)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: storageLimit is skipped here to not intoduce a surprising side effect.
|
||||
public reset(): boolean {
|
||||
// reset options customizable by sequences to defaults
|
||||
this._opts.sixelScrolling = this._defaultOpts.sixelScrolling;
|
||||
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
|
||||
// also clear image storage
|
||||
this._storage?.reset();
|
||||
// reset protocol handlers
|
||||
for (const handler of this._handlers.values()) {
|
||||
handler.reset();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public get storageLimit(): number {
|
||||
return this._storage?.getLimit() || -1;
|
||||
}
|
||||
|
||||
public set storageLimit(limit: number) {
|
||||
this._storage?.setLimit(limit);
|
||||
this._opts.storageLimit = limit;
|
||||
}
|
||||
|
||||
public get storageUsage(): number {
|
||||
if (this._storage) {
|
||||
return this._storage.getUsage();
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public get showPlaceholder(): boolean {
|
||||
return this._opts.showPlaceholder;
|
||||
}
|
||||
|
||||
public set showPlaceholder(value: boolean) {
|
||||
this._opts.showPlaceholder = value;
|
||||
this._renderer?.showPlaceholder(value);
|
||||
}
|
||||
|
||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||
return this._storage?.getImageAtBufferCell(x, y);
|
||||
}
|
||||
|
||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||
return this._storage?.extractTileAtBufferCell(x, y);
|
||||
}
|
||||
|
||||
private _report(s: string): void {
|
||||
this._terminal?._core.coreService.triggerDataEvent(s);
|
||||
}
|
||||
|
||||
private _decset(params: (number | number[])[]): boolean {
|
||||
for (let i = 0; i < params.length; ++i) {
|
||||
switch (params[i]) {
|
||||
case 80:
|
||||
this._opts.sixelScrolling = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _decrst(params: (number | number[])[]): boolean {
|
||||
for (let i = 0; i < params.length; ++i) {
|
||||
switch (params[i]) {
|
||||
case 80:
|
||||
this._opts.sixelScrolling = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// overload DA to return something more appropriate
|
||||
private _da1(params: (number | number[])[]): boolean {
|
||||
if (params[0]) {
|
||||
return true;
|
||||
}
|
||||
// reported features:
|
||||
// 62 - VT220
|
||||
// 4 - SIXEL support
|
||||
// 9 - charsets
|
||||
// 22 - ANSI colors
|
||||
if (this._opts.sixelSupport) {
|
||||
this._report(`\x1b[?62;4;9;22c`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implementation of xterm's graphics attribute sequence.
|
||||
*
|
||||
* Supported features:
|
||||
* - read/change palette limits (max 4096 by sixel lib)
|
||||
* - read SIXEL canvas geometry (reports current window canvas or
|
||||
* squared pixelLimit if canvas > pixel limit)
|
||||
*
|
||||
* Everything else is deactivated.
|
||||
*/
|
||||
private _xtermGraphicsAttributes(params: (number | number[])[]): boolean {
|
||||
if (params.length < 2) {
|
||||
return true;
|
||||
}
|
||||
if (params[0] === GaItem.COLORS) {
|
||||
switch (params[1]) {
|
||||
case GaAction.READ:
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
||||
return true;
|
||||
case GaAction.SET_DEFAULT:
|
||||
this._opts.sixelPaletteLimit = this._defaultOpts.sixelPaletteLimit;
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
||||
// also reset protocol handlers for now
|
||||
for (const handler of this._handlers.values()) {
|
||||
handler.reset();
|
||||
}
|
||||
return true;
|
||||
case GaAction.SET:
|
||||
if (params.length > 2 && !(params[2] instanceof Array) && params[2] <= MAX_SIXEL_PALETTE_SIZE) {
|
||||
this._opts.sixelPaletteLimit = params[2];
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${this._opts.sixelPaletteLimit}S`);
|
||||
} else {
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
||||
}
|
||||
return true;
|
||||
case GaAction.READ_MAX:
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${MAX_SIXEL_PALETTE_SIZE}S`);
|
||||
return true;
|
||||
default:
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (params[0] === GaItem.SIXEL_GEO) {
|
||||
switch (params[1]) {
|
||||
// we only implement read and read_max here
|
||||
case GaAction.READ:
|
||||
let width = this._renderer?.dimensions?.css.canvas.width;
|
||||
let height = this._renderer?.dimensions?.css.canvas.height;
|
||||
if (!width || !height) {
|
||||
// for some reason we have no working image renderer
|
||||
// --> fallback to default cell size
|
||||
const cellSize = CELL_SIZE_DEFAULT;
|
||||
width = (this._terminal?.cols || 80) * cellSize.width;
|
||||
height = (this._terminal?.rows || 24) * cellSize.height;
|
||||
}
|
||||
if (width * height < this._opts.pixelLimit) {
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${width.toFixed(0)};${height.toFixed(0)}S`);
|
||||
} else {
|
||||
// if we overflow pixelLimit report that squared instead
|
||||
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
|
||||
}
|
||||
return true;
|
||||
case GaAction.READ_MAX:
|
||||
// read_max returns pixelLimit as square area
|
||||
const x = Math.floor(Math.sqrt(this._opts.pixelLimit));
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.SUCCESS};${x};${x}S`);
|
||||
return true;
|
||||
default:
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.ACTION_ERROR}S`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// exit with error on ReGIS or any other requests
|
||||
this._report(`\x1b[?${params[0]};${GaStatus.ITEM_ERROR}S`);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { toRGBA8888 } from 'sixel/lib/Colors';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { ICellSize, ITerminalExt, IImageSpec, IRenderDimensions, IRenderService } from './Types';
|
||||
import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
|
||||
const PLACEHOLDER_LENGTH = 4096;
|
||||
const PLACEHOLDER_HEIGHT = 24;
|
||||
|
||||
/**
|
||||
* ImageRenderer - terminal frontend extension:
|
||||
* - provide primitives for canvas, ImageData, Bitmap (static)
|
||||
* - add canvas layer to DOM (browser only for now)
|
||||
* - draw image tiles onRender
|
||||
*/
|
||||
export class ImageRenderer extends Disposable implements IDisposable {
|
||||
public canvas: HTMLCanvasElement | undefined;
|
||||
private _ctx: CanvasRenderingContext2D | null | undefined;
|
||||
private _placeholder: HTMLCanvasElement | undefined;
|
||||
private _placeholderBitmap: ImageBitmap | undefined;
|
||||
private _optionsRefresh = this.register(new MutableDisposable());
|
||||
private _oldOpen: ((parent: HTMLElement) => void) | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _oldSetRenderer: ((renderer: any) => void) | undefined;
|
||||
|
||||
// drawing primitive - canvas
|
||||
public static createCanvas(localDocument: Document | undefined, width: number, height: number): HTMLCanvasElement {
|
||||
/**
|
||||
* NOTE: We normally dont care, from which document the canvas
|
||||
* gets created, so we can fall back to global document,
|
||||
* if the terminal has no document associated yet.
|
||||
* This way early image loads before calling .open keep working
|
||||
* (still discouraged though, as the metrics will be screwed up).
|
||||
* Only the DOM output canvas should be on the terminal's document,
|
||||
* which gets explicitly checked in `insertLayerToDom`.
|
||||
*/
|
||||
const canvas = (localDocument || document).createElement('canvas');
|
||||
canvas.width = width | 0;
|
||||
canvas.height = height | 0;
|
||||
return canvas;
|
||||
}
|
||||
|
||||
// drawing primitive - ImageData with optional buffer
|
||||
public static createImageData(ctx: CanvasRenderingContext2D, width: number, height: number, buffer?: ArrayBuffer): ImageData {
|
||||
if (typeof ImageData !== 'function') {
|
||||
const imgData = ctx.createImageData(width, height);
|
||||
if (buffer) {
|
||||
imgData.data.set(new Uint8ClampedArray(buffer, 0, width * height * 4));
|
||||
}
|
||||
return imgData;
|
||||
}
|
||||
return buffer
|
||||
? new ImageData(new Uint8ClampedArray(buffer, 0, width * height * 4), width, height)
|
||||
: new ImageData(width, height);
|
||||
}
|
||||
|
||||
// drawing primitive - ImageBitmap
|
||||
public static createImageBitmap(img: ImageBitmapSource): Promise<ImageBitmap | undefined> {
|
||||
if (typeof createImageBitmap !== 'function') {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return createImageBitmap(img);
|
||||
}
|
||||
|
||||
|
||||
constructor(private _terminal: ITerminalExt) {
|
||||
super();
|
||||
this._oldOpen = this._terminal._core.open;
|
||||
this._terminal._core.open = (parent: HTMLElement): void => {
|
||||
this._oldOpen?.call(this._terminal._core, parent);
|
||||
this._open();
|
||||
};
|
||||
if (this._terminal._core.screenElement) {
|
||||
this._open();
|
||||
}
|
||||
// hack to spot fontSize changes
|
||||
this._optionsRefresh.value = this._terminal._core.optionsService.onOptionChange(option => {
|
||||
if (option === 'fontSize') {
|
||||
this.rescaleCanvas();
|
||||
this._renderService?.refreshRows(0, this._terminal.rows);
|
||||
}
|
||||
});
|
||||
this.register(toDisposable(() => {
|
||||
this.removeLayerFromDom();
|
||||
if (this._terminal._core && this._oldOpen) {
|
||||
this._terminal._core.open = this._oldOpen;
|
||||
this._oldOpen = undefined;
|
||||
}
|
||||
if (this._renderService && this._oldSetRenderer) {
|
||||
this._renderService.setRenderer = this._oldSetRenderer;
|
||||
this._oldSetRenderer = undefined;
|
||||
}
|
||||
this._renderService = undefined;
|
||||
this.canvas = undefined;
|
||||
this._ctx = undefined;
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
this._placeholder = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the placeholder.
|
||||
*/
|
||||
public showPlaceholder(value: boolean): void {
|
||||
if (value) {
|
||||
if (!this._placeholder && this.cellSize.height !== -1) {
|
||||
this._createPlaceHolder(Math.max(this.cellSize.height + 1, PLACEHOLDER_HEIGHT));
|
||||
}
|
||||
} else {
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
this._placeholder = undefined;
|
||||
}
|
||||
this._renderService?.refreshRows(0, this._terminal.rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dimensions of the terminal.
|
||||
* Forwarded from internal render service.
|
||||
*/
|
||||
public get dimensions(): IRenderDimensions | undefined {
|
||||
return this._renderService?.dimensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Current cell size (float).
|
||||
*/
|
||||
public get cellSize(): ICellSize {
|
||||
return {
|
||||
width: this.dimensions?.css.cell.width || -1,
|
||||
height: this.dimensions?.css.cell.height || -1
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear a region of the image layer canvas.
|
||||
*/
|
||||
public clearLines(start: number, end: number): void {
|
||||
this._ctx?.clearRect(
|
||||
0,
|
||||
start * (this.dimensions?.css.cell.height || 0),
|
||||
this.dimensions?.css.canvas.width || 0,
|
||||
(++end - start) * (this.dimensions?.css.cell.height || 0)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear whole image canvas.
|
||||
*/
|
||||
public clearAll(): void {
|
||||
this._ctx?.clearRect(0, 0, this.canvas?.width || 0, this.canvas?.height || 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw neighboring tiles on the image layer canvas.
|
||||
*/
|
||||
public draw(imgSpec: IImageSpec, tileId: number, col: number, row: number, count: number = 1): void {
|
||||
if (!this._ctx) {
|
||||
return;
|
||||
}
|
||||
const { width, height } = this.cellSize;
|
||||
|
||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
||||
if (width === -1 || height === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._rescaleImage(imgSpec, width, height);
|
||||
const img = imgSpec.actual!;
|
||||
const cols = Math.ceil(img.width / width);
|
||||
|
||||
const sx = (tileId % cols) * width;
|
||||
const sy = Math.floor(tileId / cols) * height;
|
||||
const dx = col * width;
|
||||
const dy = row * height;
|
||||
|
||||
// safari bug: never access image source out of bounds
|
||||
const finalWidth = count * width + sx > img.width ? img.width - sx : count * width;
|
||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
||||
|
||||
// Floor all pixel offsets to get stable tile mapping without any overflows.
|
||||
// Note: For not pixel perfect aligned cells like in the DOM renderer
|
||||
// this will move a tile slightly to the top/left (subpixel range, thus ignore it).
|
||||
// FIX #34: avoid striping on displays with pixelDeviceRatio != 1 by ceiling height and width
|
||||
this._ctx.drawImage(
|
||||
img,
|
||||
Math.floor(sx), Math.floor(sy), Math.ceil(finalWidth), Math.ceil(finalHeight),
|
||||
Math.floor(dx), Math.floor(dy), Math.ceil(finalWidth), Math.ceil(finalHeight)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a single tile from an image.
|
||||
*/
|
||||
public extractTile(imgSpec: IImageSpec, tileId: number): HTMLCanvasElement | undefined {
|
||||
const { width, height } = this.cellSize;
|
||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
||||
if (width === -1 || height === -1) {
|
||||
return;
|
||||
}
|
||||
this._rescaleImage(imgSpec, width, height);
|
||||
const img = imgSpec.actual!;
|
||||
const cols = Math.ceil(img.width / width);
|
||||
const sx = (tileId % cols) * width;
|
||||
const sy = Math.floor(tileId / cols) * height;
|
||||
const finalWidth = width + sx > img.width ? img.width - sx : width;
|
||||
const finalHeight = sy + height > img.height ? img.height - sy : height;
|
||||
|
||||
const canvas = ImageRenderer.createCanvas(this.document, finalWidth, finalHeight);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(
|
||||
img,
|
||||
Math.floor(sx), Math.floor(sy), Math.floor(finalWidth), Math.floor(finalHeight),
|
||||
0, 0, Math.floor(finalWidth), Math.floor(finalHeight)
|
||||
);
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line with placeholder on the image layer canvas.
|
||||
*/
|
||||
public drawPlaceholder(col: number, row: number, count: number = 1): void {
|
||||
if (this._ctx) {
|
||||
const { width, height } = this.cellSize;
|
||||
|
||||
// Don't try to draw anything, if we cannot get valid renderer metrics.
|
||||
if (width === -1 || height === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._placeholder) {
|
||||
this._createPlaceHolder(Math.max(height + 1, PLACEHOLDER_HEIGHT));
|
||||
} else if (height >= this._placeholder!.height) {
|
||||
this._createPlaceHolder(height + 1);
|
||||
}
|
||||
if (!this._placeholder) return;
|
||||
this._ctx.drawImage(
|
||||
this._placeholderBitmap || this._placeholder!,
|
||||
col * width,
|
||||
(row * height) % 2 ? 0 : 1, // needs %2 offset correction
|
||||
width * count,
|
||||
height,
|
||||
col * width,
|
||||
row * height,
|
||||
width * count,
|
||||
height
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescale image layer canvas if needed.
|
||||
* Checked once from `ImageStorage.render`.
|
||||
*/
|
||||
public rescaleCanvas(): void {
|
||||
if (!this.canvas) {
|
||||
return;
|
||||
}
|
||||
if (this.canvas.width !== this.dimensions!.css.canvas.width || this.canvas.height !== this.dimensions!.css.canvas.height) {
|
||||
this.canvas.width = this.dimensions!.css.canvas.width || 0;
|
||||
this.canvas.height = this.dimensions!.css.canvas.height || 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rescale image in storage if needed.
|
||||
*/
|
||||
private _rescaleImage(spec: IImageSpec, currentWidth: number, currentHeight: number): void {
|
||||
if (currentWidth === spec.actualCellSize.width && currentHeight === spec.actualCellSize.height) {
|
||||
return;
|
||||
}
|
||||
const { width: originalWidth, height: originalHeight } = spec.origCellSize;
|
||||
if (currentWidth === originalWidth && currentHeight === originalHeight) {
|
||||
spec.actual = spec.orig;
|
||||
spec.actualCellSize.width = originalWidth;
|
||||
spec.actualCellSize.height = originalHeight;
|
||||
return;
|
||||
}
|
||||
const canvas = ImageRenderer.createCanvas(
|
||||
this.document,
|
||||
Math.ceil(spec.orig!.width * currentWidth / originalWidth),
|
||||
Math.ceil(spec.orig!.height * currentHeight / originalHeight)
|
||||
);
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(spec.orig!, 0, 0, canvas.width, canvas.height);
|
||||
spec.actual = canvas;
|
||||
spec.actualCellSize.width = currentWidth;
|
||||
spec.actualCellSize.height = currentHeight;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy init for the renderer.
|
||||
*/
|
||||
private _open(): void {
|
||||
this._renderService = this._terminal._core._renderService;
|
||||
this._oldSetRenderer = this._renderService.setRenderer.bind(this._renderService);
|
||||
this._renderService.setRenderer = (renderer: any) => {
|
||||
this.removeLayerFromDom();
|
||||
this._oldSetRenderer?.call(this._renderService, renderer);
|
||||
};
|
||||
}
|
||||
|
||||
public insertLayerToDom(): void {
|
||||
// make sure that the terminal is attached to a document and to DOM
|
||||
if (this.document && this._terminal._core.screenElement) {
|
||||
if (!this.canvas) {
|
||||
this.canvas = ImageRenderer.createCanvas(
|
||||
this.document, this.dimensions?.css.canvas.width || 0,
|
||||
this.dimensions?.css.canvas.height || 0
|
||||
);
|
||||
this.canvas.classList.add('xterm-image-layer');
|
||||
this._terminal._core.screenElement.appendChild(this.canvas);
|
||||
this._ctx = this.canvas.getContext('2d', { alpha: true, desynchronized: true });
|
||||
this.clearAll();
|
||||
}
|
||||
} else {
|
||||
console.warn('image addon: cannot insert output canvas to DOM, missing document or screenElement');
|
||||
}
|
||||
}
|
||||
|
||||
public removeLayerFromDom(): void {
|
||||
if (this.canvas) {
|
||||
this._ctx = undefined;
|
||||
this.canvas.remove();
|
||||
this.canvas = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _createPlaceHolder(height: number = PLACEHOLDER_HEIGHT): void {
|
||||
this._placeholderBitmap?.close();
|
||||
this._placeholderBitmap = undefined;
|
||||
|
||||
// create blueprint to fill placeholder with
|
||||
const bWidth = 32; // must be 2^n
|
||||
const blueprint = ImageRenderer.createCanvas(this.document, bWidth, height);
|
||||
const ctx = blueprint.getContext('2d', { alpha: false });
|
||||
if (!ctx) return;
|
||||
const imgData = ImageRenderer.createImageData(ctx, bWidth, height);
|
||||
const d32 = new Uint32Array(imgData.data.buffer);
|
||||
const black = toRGBA8888(0, 0, 0);
|
||||
const white = toRGBA8888(255, 255, 255);
|
||||
d32.fill(black);
|
||||
for (let y = 0; y < height; ++y) {
|
||||
const shift = y % 2;
|
||||
const offset = y * bWidth;
|
||||
for (let x = 0; x < bWidth; x += 2) {
|
||||
d32[offset + x + shift] = white;
|
||||
}
|
||||
}
|
||||
ctx.putImageData(imgData, 0, 0);
|
||||
|
||||
// create placeholder line, width aligned to blueprint width
|
||||
const width = (screen.width + bWidth - 1) & ~(bWidth - 1) || PLACEHOLDER_LENGTH;
|
||||
this._placeholder = ImageRenderer.createCanvas(this.document, width, height);
|
||||
const ctx2 = this._placeholder.getContext('2d', { alpha: false });
|
||||
if (!ctx2) {
|
||||
this._placeholder = undefined;
|
||||
return;
|
||||
}
|
||||
for (let i = 0; i < width; i += bWidth) {
|
||||
ctx2.drawImage(blueprint, i, 0);
|
||||
}
|
||||
ImageRenderer.createImageBitmap(this._placeholder).then(bitmap => this._placeholderBitmap = bitmap);
|
||||
}
|
||||
|
||||
public get document(): Document | undefined {
|
||||
return this._terminal._core._coreBrowserService?.window.document;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
import { ITerminalExt, IExtendedAttrsImage, IImageAddonOptions, IImageSpec, IBufferLineExt, BgFlags, Cell, Content, ICellSize, ExtFlags, Attributes, UnderlineStyle } from './Types';
|
||||
|
||||
|
||||
// fallback default cell size
|
||||
export const CELL_SIZE_DEFAULT: ICellSize = {
|
||||
width: 7,
|
||||
height: 14
|
||||
};
|
||||
|
||||
/**
|
||||
* Extend extended attribute to also hold image tile information.
|
||||
*
|
||||
* Object definition is copied from base repo to fully mimick its behavior.
|
||||
* Image data is added as additional public properties `imageId` and `tileId`.
|
||||
*/
|
||||
class ExtendedAttrsImage implements IExtendedAttrsImage {
|
||||
private _ext: number = 0;
|
||||
public get ext(): number {
|
||||
if (this._urlId) {
|
||||
return (
|
||||
(this._ext & ~ExtFlags.UNDERLINE_STYLE) |
|
||||
(this.underlineStyle << 26)
|
||||
);
|
||||
}
|
||||
return this._ext;
|
||||
}
|
||||
public set ext(value: number) { this._ext = value; }
|
||||
|
||||
public get underlineStyle(): UnderlineStyle {
|
||||
// Always return the URL style if it has one
|
||||
if (this._urlId) {
|
||||
return UnderlineStyle.DASHED;
|
||||
}
|
||||
return (this._ext & ExtFlags.UNDERLINE_STYLE) >> 26;
|
||||
}
|
||||
public set underlineStyle(value: UnderlineStyle) {
|
||||
this._ext &= ~ExtFlags.UNDERLINE_STYLE;
|
||||
this._ext |= (value << 26) & ExtFlags.UNDERLINE_STYLE;
|
||||
}
|
||||
|
||||
public get underlineColor(): number {
|
||||
return this._ext & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
}
|
||||
public set underlineColor(value: number) {
|
||||
this._ext &= ~(Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
this._ext |= value & (Attributes.CM_MASK | Attributes.RGB_MASK);
|
||||
}
|
||||
|
||||
private _urlId: number = 0;
|
||||
public get urlId(): number {
|
||||
return this._urlId;
|
||||
}
|
||||
public set urlId(value: number) {
|
||||
this._urlId = value;
|
||||
}
|
||||
|
||||
constructor(
|
||||
ext: number = 0,
|
||||
urlId: number = 0,
|
||||
public imageId = -1,
|
||||
public tileId = -1
|
||||
) {
|
||||
this._ext = ext;
|
||||
this._urlId = urlId;
|
||||
}
|
||||
|
||||
public clone(): IExtendedAttrsImage {
|
||||
/**
|
||||
* Technically we dont need a clone variant of ExtendedAttrsImage,
|
||||
* as we never clone a cell holding image data.
|
||||
* Note: Clone is only meant to be used by the InputHandler for
|
||||
* sticky attributes, which is never the case for image data.
|
||||
* We still provide a proper clone method to reflect the full ext attr
|
||||
* state in case there are future use cases for clone.
|
||||
*/
|
||||
return new ExtendedAttrsImage(this._ext, this._urlId, this.imageId, this.tileId);
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return this.underlineStyle === UnderlineStyle.NONE && this._urlId === 0 && this.imageId === -1;
|
||||
}
|
||||
}
|
||||
const EMPTY_ATTRS = new ExtendedAttrsImage();
|
||||
|
||||
|
||||
/**
|
||||
* ImageStorage - extension of CoreTerminal:
|
||||
* - hold image data
|
||||
* - write/read image data to/from buffer
|
||||
*
|
||||
* TODO: image composition for overwrites
|
||||
*/
|
||||
export class ImageStorage implements IDisposable {
|
||||
// storage
|
||||
private _images: Map<number, IImageSpec> = new Map();
|
||||
// last used id
|
||||
private _lastId = 0;
|
||||
// last evicted id
|
||||
private _lowestId = 0;
|
||||
// whether a full clear happened before
|
||||
private _fullyCleared = false;
|
||||
// whether render should do a full clear
|
||||
private _needsFullClear = false;
|
||||
// hard limit of stored pixels (fallback limit of 10 MB)
|
||||
private _pixelLimit: number = 2500000;
|
||||
|
||||
private _viewportMetrics: { cols: number, rows: number };
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminalExt,
|
||||
private _renderer: ImageRenderer,
|
||||
private _opts: IImageAddonOptions
|
||||
) {
|
||||
try {
|
||||
this.setLimit(this._opts.storageLimit);
|
||||
} catch (e: any) {
|
||||
console.error(e.message);
|
||||
console.warn(`storageLimit is set to ${this.getLimit()} MB`);
|
||||
}
|
||||
this._viewportMetrics = {
|
||||
cols: this._terminal.cols,
|
||||
rows: this._terminal.rows
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
for (const spec of this._images.values()) {
|
||||
spec.marker?.dispose();
|
||||
}
|
||||
// NOTE: marker.dispose above already calls ImageBitmap.close
|
||||
// therefore we can just wipe the map here
|
||||
this._images.clear();
|
||||
this._renderer.clearAll();
|
||||
}
|
||||
|
||||
public getLimit(): number {
|
||||
return this._pixelLimit * 4 / 1000000;
|
||||
}
|
||||
|
||||
public setLimit(value: number): void {
|
||||
if (value < 0.5 || value > 1000) {
|
||||
throw RangeError('invalid storageLimit, should be at least 0.5 MB and not exceed 1G');
|
||||
}
|
||||
this._pixelLimit = (value / 4 * 1000000) >>> 0;
|
||||
this._evictOldest(0);
|
||||
}
|
||||
|
||||
public getUsage(): number {
|
||||
return this._getStoredPixels() * 4 / 1000000;
|
||||
}
|
||||
|
||||
private _getStoredPixels(): number {
|
||||
let storedPixels = 0;
|
||||
for (const spec of this._images.values()) {
|
||||
if (spec.orig) {
|
||||
storedPixels += spec.orig.width * spec.orig.height;
|
||||
if (spec.actual && spec.actual !== spec.orig) {
|
||||
storedPixels += spec.actual.width * spec.actual.height;
|
||||
}
|
||||
}
|
||||
}
|
||||
return storedPixels;
|
||||
}
|
||||
|
||||
private _delImg(id: number): void {
|
||||
const spec = this._images.get(id);
|
||||
this._images.delete(id);
|
||||
// FIXME: really ugly workaround to get bitmaps deallocated :(
|
||||
if (spec && window.ImageBitmap && spec.orig instanceof ImageBitmap) {
|
||||
spec.orig.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wipe canvas and images on alternate buffer.
|
||||
*/
|
||||
public wipeAlternate(): void {
|
||||
// remove all alternate tagged images
|
||||
const zero = [];
|
||||
for (const [id, spec] of this._images.entries()) {
|
||||
if (spec.bufferType === 'alternate') {
|
||||
spec.marker?.dispose();
|
||||
zero.push(id);
|
||||
}
|
||||
}
|
||||
for (const id of zero) {
|
||||
this._delImg(id);
|
||||
}
|
||||
// mark canvas to be wiped on next render
|
||||
this._needsFullClear = true;
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Only advance text cursor.
|
||||
* This is an edge case from empty sixels carrying only a height but no pixels.
|
||||
* Partially fixes https://github.com/jerch/xterm-addon-image/issues/37.
|
||||
*/
|
||||
public advanceCursor(height: number): void {
|
||||
if (this._opts.sixelScrolling) {
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const rows = Math.ceil(height / cellSize.height);
|
||||
for (let i = 1; i < rows; ++i) {
|
||||
this._terminal._core._inputHandler.lineFeed();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add an image to the storage.
|
||||
*/
|
||||
public addImage(img: HTMLCanvasElement | ImageBitmap): void {
|
||||
// never allow storage to exceed memory limit
|
||||
this._evictOldest(img.width * img.height);
|
||||
|
||||
// calc rows x cols needed to display the image
|
||||
let cellSize = this._renderer.cellSize;
|
||||
if (cellSize.width === -1 || cellSize.height === -1) {
|
||||
cellSize = CELL_SIZE_DEFAULT;
|
||||
}
|
||||
const cols = Math.ceil(img.width / cellSize.width);
|
||||
const rows = Math.ceil(img.height / cellSize.height);
|
||||
|
||||
const imageId = ++this._lastId;
|
||||
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const termCols = this._terminal.cols;
|
||||
const termRows = this._terminal.rows;
|
||||
const originX = buffer.x;
|
||||
const originY = buffer.y;
|
||||
let offset = originX;
|
||||
let tileCount = 0;
|
||||
|
||||
if (!this._opts.sixelScrolling) {
|
||||
buffer.x = 0;
|
||||
buffer.y = 0;
|
||||
offset = 0;
|
||||
}
|
||||
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(buffer.y + buffer.ybase);
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
if (offset + col >= termCols) break;
|
||||
this._writeToCell(line as IBufferLineExt, offset + col, imageId, row * cols + col);
|
||||
tileCount++;
|
||||
}
|
||||
if (this._opts.sixelScrolling) {
|
||||
if (row < rows - 1) this._terminal._core._inputHandler.lineFeed();
|
||||
} else {
|
||||
if (++buffer.y >= termRows) break;
|
||||
}
|
||||
buffer.x = offset;
|
||||
}
|
||||
this._terminal._core._inputHandler._dirtyRowTracker.markDirty(buffer.y);
|
||||
|
||||
// cursor positioning modes
|
||||
if (this._opts.sixelScrolling) {
|
||||
buffer.x = offset;
|
||||
} else {
|
||||
buffer.x = originX;
|
||||
buffer.y = originY;
|
||||
}
|
||||
|
||||
// deleted images with zero tile count
|
||||
const zero = [];
|
||||
for (const [id, spec] of this._images.entries()) {
|
||||
if (spec.tileCount < 1) {
|
||||
spec.marker?.dispose();
|
||||
zero.push(id);
|
||||
}
|
||||
}
|
||||
for (const id of zero) {
|
||||
this._delImg(id);
|
||||
}
|
||||
|
||||
// eviction marker:
|
||||
// delete the image when the marker gets disposed
|
||||
const endMarker = this._terminal.registerMarker(0);
|
||||
endMarker?.onDispose(() => {
|
||||
const spec = this._images.get(imageId);
|
||||
if (spec) {
|
||||
this._delImg(imageId);
|
||||
}
|
||||
});
|
||||
|
||||
// since markers do not work on alternate for some reason,
|
||||
// we evict images here manually
|
||||
if (this._terminal.buffer.active.type === 'alternate') {
|
||||
this._evictOnAlternate();
|
||||
}
|
||||
|
||||
// create storage entry
|
||||
const imgSpec: IImageSpec = {
|
||||
orig: img,
|
||||
origCellSize: cellSize,
|
||||
actual: img,
|
||||
actualCellSize: { ...cellSize }, // clone needed, since later modified
|
||||
marker: endMarker || undefined,
|
||||
tileCount,
|
||||
bufferType: this._terminal.buffer.active.type
|
||||
};
|
||||
|
||||
// finally add the image
|
||||
this._images.set(imageId, imgSpec);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Render method. Collects buffer information and triggers
|
||||
* canvas updates.
|
||||
*/
|
||||
// TODO: Should we move this to the ImageRenderer?
|
||||
public render(range: { start: number, end: number }): void {
|
||||
// setup image canvas in case we have none yet, but have images in store
|
||||
if (!this._renderer.canvas && this._images.size) {
|
||||
this._renderer.insertLayerToDom();
|
||||
// safety measure - in case we cannot spawn a canvas at all, just exit
|
||||
if (!this._renderer.canvas) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// rescale if needed
|
||||
this._renderer.rescaleCanvas();
|
||||
// exit early if we dont have any images to test for
|
||||
if (!this._images.size) {
|
||||
if (!this._fullyCleared) {
|
||||
this._renderer.clearAll();
|
||||
this._fullyCleared = true;
|
||||
this._needsFullClear = false;
|
||||
}
|
||||
if (this._renderer.canvas) {
|
||||
this._renderer.removeLayerFromDom();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// buffer switches force a full clear
|
||||
if (this._needsFullClear) {
|
||||
this._renderer.clearAll();
|
||||
this._fullyCleared = true;
|
||||
this._needsFullClear = false;
|
||||
}
|
||||
|
||||
const { start, end } = range;
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const cols = this._terminal._core.cols;
|
||||
|
||||
// clear drawing area
|
||||
this._renderer.clearLines(start, end);
|
||||
|
||||
// walk all cells in viewport and draw tiles found
|
||||
for (let row = start; row <= end; ++row) {
|
||||
const line = buffer.lines.get(row + buffer.ydisp) as IBufferLineExt;
|
||||
if (!line) return;
|
||||
for (let col = 0; col < cols; ++col) {
|
||||
if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
|
||||
let e: IExtendedAttrsImage = line._extendedAttrs[col] || EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (e.tileId !== -1) {
|
||||
const startTile = e.tileId;
|
||||
const startCol = col;
|
||||
let count = 1;
|
||||
/**
|
||||
* merge tiles to the right into a single draw call, if:
|
||||
* - not at end of line
|
||||
* - cell has same image id
|
||||
* - cell has consecutive tile id
|
||||
*/
|
||||
while (
|
||||
++col < cols
|
||||
&& (line.getBg(col) & BgFlags.HAS_EXTENDED)
|
||||
&& (e = line._extendedAttrs[col] || EMPTY_ATTRS)
|
||||
&& (e.imageId === imageId)
|
||||
&& (e.tileId === startTile + count)
|
||||
) {
|
||||
count++;
|
||||
}
|
||||
col--;
|
||||
if (imgSpec) {
|
||||
if (imgSpec.actual) {
|
||||
this._renderer.draw(imgSpec, startTile, startCol, row, count);
|
||||
}
|
||||
} else if (this._opts.showPlaceholder) {
|
||||
this._renderer.drawPlaceholder(startCol, row, count);
|
||||
}
|
||||
this._fullyCleared = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public viewportResize(metrics: { cols: number, rows: number }): void {
|
||||
// exit early if we have nothing in storage
|
||||
if (!this._images.size) {
|
||||
this._viewportMetrics = metrics;
|
||||
return;
|
||||
}
|
||||
|
||||
// handle only viewport width enlargements, exit all other cases
|
||||
// TODO: needs patch for tile counter
|
||||
if (this._viewportMetrics.cols >= metrics.cols) {
|
||||
this._viewportMetrics = metrics;
|
||||
return;
|
||||
}
|
||||
|
||||
// walk scrollbuffer at old col width to find all possible expansion matches
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const rows = buffer.lines.length;
|
||||
const oldCol = this._viewportMetrics.cols - 1;
|
||||
for (let row = 0; row < rows; ++row) {
|
||||
const line = buffer.lines.get(row) as IBufferLineExt;
|
||||
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] || EMPTY_ATTRS;
|
||||
const imageId = e.imageId;
|
||||
if (imageId === undefined || imageId === -1) {
|
||||
continue;
|
||||
}
|
||||
const imgSpec = this._images.get(imageId);
|
||||
if (!imgSpec) {
|
||||
continue;
|
||||
}
|
||||
// found an image tile at oldCol, check if it qualifies for right exapansion
|
||||
const tilesPerRow = Math.ceil((imgSpec.actual?.width || 0) / imgSpec.actualCellSize.width);
|
||||
if ((e.tileId % tilesPerRow) + 1 >= tilesPerRow) {
|
||||
continue;
|
||||
}
|
||||
// expand only if right side is empty (nothing got wrapped from below)
|
||||
let hasData = false;
|
||||
for (let rightCol = oldCol + 1; rightCol > metrics.cols; ++rightCol) {
|
||||
if (line._data[rightCol * Cell.SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK) {
|
||||
hasData = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasData) {
|
||||
continue;
|
||||
}
|
||||
// do right expansion on terminal buffer
|
||||
const end = Math.min(metrics.cols, tilesPerRow - (e.tileId % tilesPerRow) + oldCol);
|
||||
let lastTile = e.tileId;
|
||||
for (let expandCol = oldCol + 1; expandCol < end; ++expandCol) {
|
||||
this._writeToCell(line as IBufferLineExt, expandCol, imageId, ++lastTile);
|
||||
imgSpec.tileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
// store new viewport metrics
|
||||
this._viewportMetrics = metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve original canvas at buffer position.
|
||||
*/
|
||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1) {
|
||||
const orig = this._images.get(e.imageId)?.orig;
|
||||
if (window.ImageBitmap && orig instanceof ImageBitmap) {
|
||||
const canvas = ImageRenderer.createCanvas(window.document, orig.width, orig.height);
|
||||
canvas.getContext('2d')?.drawImage(orig, 0, 0, orig.width, orig.height);
|
||||
return canvas;
|
||||
}
|
||||
return orig as HTMLCanvasElement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract active single tile at buffer position.
|
||||
*/
|
||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined {
|
||||
const buffer = this._terminal._core.buffer;
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
|
||||
const e: IExtendedAttrsImage = line._extendedAttrs[x] || EMPTY_ATTRS;
|
||||
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
|
||||
const spec = this._images.get(e.imageId);
|
||||
if (spec) {
|
||||
return this._renderer.extractTile(spec, e.tileId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Do we need some blob offloading tricks here to avoid early eviction?
|
||||
// also see https://stackoverflow.com/questions/28307789/is-there-any-limitation-on-javascript-max-blob-size
|
||||
private _evictOldest(room: number): number {
|
||||
const used = this._getStoredPixels();
|
||||
let current = used;
|
||||
while (this._pixelLimit < current + room && this._images.size) {
|
||||
const spec = this._images.get(++this._lowestId);
|
||||
if (spec && spec.orig) {
|
||||
current -= spec.orig.width * spec.orig.height;
|
||||
if (spec.actual && spec.orig !== spec.actual) {
|
||||
current -= spec.actual.width * spec.actual.height;
|
||||
}
|
||||
spec.marker?.dispose();
|
||||
this._delImg(this._lowestId);
|
||||
}
|
||||
}
|
||||
return used - current;
|
||||
}
|
||||
|
||||
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const old = line._extendedAttrs[x];
|
||||
if (old) {
|
||||
if (old.imageId !== undefined) {
|
||||
// found an old ExtendedAttrsImage, since we know that
|
||||
// they are always isolated instances (single cell usage),
|
||||
// we can re-use it and just update their id entries
|
||||
const oldSpec = this._images.get(old.imageId);
|
||||
if (oldSpec) {
|
||||
// early eviction for in-viewport overwrites
|
||||
oldSpec.tileCount--;
|
||||
}
|
||||
old.imageId = imageId;
|
||||
old.tileId = tileId;
|
||||
return;
|
||||
}
|
||||
// found a plain ExtendedAttrs instance, clone it to new entry
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// fall-through: always create new ExtendedAttrsImage entry
|
||||
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
|
||||
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
|
||||
}
|
||||
|
||||
private _evictOnAlternate(): void {
|
||||
// nullify tile count of all images on alternate buffer
|
||||
for (const spec of this._images.values()) {
|
||||
if (spec.bufferType === 'alternate') {
|
||||
spec.tileCount = 0;
|
||||
}
|
||||
}
|
||||
// re-count tiles on whole buffer
|
||||
const buffer = this._terminal._core.buffer;
|
||||
for (let y = 0; y < this._terminal.rows; ++y) {
|
||||
const line = buffer.lines.get(y) as IBufferLineExt;
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
for (let x = 0; x < this._terminal.cols; ++x) {
|
||||
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
const imgId = line._extendedAttrs[x]?.imageId;
|
||||
if (imgId) {
|
||||
const spec = this._images.get(imgId);
|
||||
if (spec) {
|
||||
spec.tileCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// deleted images with zero tile count
|
||||
const zero = [];
|
||||
for (const [id, spec] of this._images.entries()) {
|
||||
if (spec.bufferType === 'alternate' && !spec.tileCount) {
|
||||
spec.marker?.dispose();
|
||||
zero.push(id);
|
||||
}
|
||||
}
|
||||
for (const id of zero) {
|
||||
this._delImg(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* Copyright (c) 2020, 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ImageStorage } from './ImageStorage';
|
||||
import { IDcsHandler, IParams, IImageAddonOptions, ITerminalExt, AttributeData, IResetHandler, ReadonlyColorSet } from './Types';
|
||||
import { toRGBA8888, BIG_ENDIAN, PALETTE_ANSI_256, PALETTE_VT340_COLOR } from 'sixel/lib/Colors';
|
||||
import { RGBA8888 } from 'sixel/lib/Types';
|
||||
import { ImageRenderer } from './ImageRenderer';
|
||||
|
||||
import { DecoderAsync, Decoder } from 'sixel/lib/Decoder';
|
||||
|
||||
// always free decoder ressources after decoding if it exceeds this limit
|
||||
const MEM_PERMA_LIMIT = 4194304; // 1024 pixels * 1024 pixels * 4 channels = 4MB
|
||||
|
||||
// custom default palette: VT340 (lower 16 colors) + ANSI256 (up to 256) + zeroed (up to 4096)
|
||||
const DEFAULT_PALETTE = PALETTE_ANSI_256;
|
||||
DEFAULT_PALETTE.set(PALETTE_VT340_COLOR);
|
||||
|
||||
|
||||
export class SixelHandler implements IDcsHandler, IResetHandler {
|
||||
private _size = 0;
|
||||
private _aborted = false;
|
||||
private _dec: Decoder | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly _opts: IImageAddonOptions,
|
||||
private readonly _storage: ImageStorage,
|
||||
private readonly _coreTerminal: ITerminalExt
|
||||
) {
|
||||
DecoderAsync({
|
||||
memoryLimit: this._opts.pixelLimit * 4,
|
||||
palette: DEFAULT_PALETTE,
|
||||
paletteLimit: this._opts.sixelPaletteLimit
|
||||
}).then(d => this._dec = d);
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
/**
|
||||
* reset sixel decoder to defaults:
|
||||
* - release all memory
|
||||
* - nullify palette (4096)
|
||||
* - apply default palette (256)
|
||||
*/
|
||||
if (this._dec) {
|
||||
this._dec.release();
|
||||
// FIXME: missing interface on decoder to nullify full palette
|
||||
(this._dec as any)._palette.fill(0);
|
||||
this._dec.init(0, DEFAULT_PALETTE, this._opts.sixelPaletteLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public hook(params: IParams): void {
|
||||
this._size = 0;
|
||||
this._aborted = false;
|
||||
if (this._dec) {
|
||||
const fillColor = params.params[1] === 1 ? 0 : extractActiveBg(
|
||||
this._coreTerminal._core._inputHandler._curAttrData,
|
||||
this._coreTerminal._core._themeService?.colors);
|
||||
this._dec.init(fillColor, null, this._opts.sixelPaletteLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public put(data: Uint32Array, start: number, end: number): void {
|
||||
if (this._aborted || !this._dec) {
|
||||
return;
|
||||
}
|
||||
this._size += end - start;
|
||||
if (this._size > this._opts.sixelSizeLimit) {
|
||||
console.warn(`SIXEL: too much data, aborting`);
|
||||
this._aborted = true;
|
||||
this._dec.release();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._dec.decode(data, start, end);
|
||||
} catch (e) {
|
||||
console.warn(`SIXEL: error while decoding image - ${e}`);
|
||||
this._aborted = true;
|
||||
this._dec.release();
|
||||
}
|
||||
}
|
||||
|
||||
public unhook(success: boolean): boolean | Promise<boolean> {
|
||||
if (this._aborted || !success || !this._dec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const width = this._dec.width;
|
||||
const height = this._dec.height;
|
||||
|
||||
// partial fix for https://github.com/jerch/xterm-addon-image/issues/37
|
||||
if (!width || ! height) {
|
||||
if (height) {
|
||||
this._storage.advanceCursor(height);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const canvas = ImageRenderer.createCanvas(undefined, width, height);
|
||||
canvas.getContext('2d')?.putImageData(new ImageData(this._dec.data8, width, height), 0, 0);
|
||||
if (this._dec.memoryUsage > MEM_PERMA_LIMIT) {
|
||||
this._dec.release();
|
||||
}
|
||||
this._storage.addImage(canvas);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Some helpers to extract current terminal colors.
|
||||
*/
|
||||
|
||||
// get currently active background color from terminal
|
||||
// also respect INVERSE setting
|
||||
function extractActiveBg(attr: AttributeData, colors: ReadonlyColorSet | undefined): RGBA8888 {
|
||||
let bg = 0;
|
||||
if (!colors) {
|
||||
// FIXME: theme service is prolly not available yet,
|
||||
// happens if .open() was not called yet (bug in core?)
|
||||
return bg;
|
||||
}
|
||||
if (attr.isInverse()) {
|
||||
if (attr.isFgDefault()) {
|
||||
bg = convertLe(colors.foreground.rgba);
|
||||
} else if (attr.isFgRGB()) {
|
||||
const t = (attr.constructor as typeof AttributeData).toColorRGB(attr.getFgColor());
|
||||
bg = toRGBA8888(...t);
|
||||
} else {
|
||||
bg = convertLe(colors.ansi[attr.getFgColor()].rgba);
|
||||
}
|
||||
} else {
|
||||
if (attr.isBgDefault()) {
|
||||
bg = convertLe(colors.background.rgba);
|
||||
} else if (attr.isBgRGB()) {
|
||||
const t = (attr.constructor as typeof AttributeData).toColorRGB(attr.getBgColor());
|
||||
bg = toRGBA8888(...t);
|
||||
} else {
|
||||
bg = convertLe(colors.ansi[attr.getBgColor()].rgba);
|
||||
}
|
||||
}
|
||||
return bg;
|
||||
}
|
||||
|
||||
// rgba values on the color managers are always in BE, thus convert to LE
|
||||
function convertLe(color: number): RGBA8888 {
|
||||
if (BIG_ENDIAN) return color;
|
||||
return (color & 0xFF) << 24 | (color >>> 8 & 0xFF) << 16 | (color >>> 16 & 0xFF) << 8 | color >>> 24 & 0xFF;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable, IMarker, Terminal } from 'xterm';
|
||||
|
||||
// private imports from base repo we build against
|
||||
import { Attributes, BgFlags, Content, ExtFlags, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import type { AttributeData } from 'common/buffer/AttributeData';
|
||||
import type { IParams, IDcsHandler, IOscHandler, IEscapeSequenceParser } from 'common/parser/Types';
|
||||
import type { IBufferLine, IExtendedAttrs, IInputHandler } from 'common/Types';
|
||||
import type { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import type { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import type { ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
|
||||
export const enum Cell {
|
||||
CONTENT = 0, // codepoint and wcwidth information (enum Content)
|
||||
FG = 1, // foreground color in lower 3 bytes (rgb), attrs in 4th byte (enum FgFlags)
|
||||
BG = 2, // background color in lower 3 bytes (rgb), attrs in 4th byte (enum BgFlags)
|
||||
SIZE = 3 // size of single cell on buffer array
|
||||
}
|
||||
|
||||
// export some privates for local usage
|
||||
export { AttributeData, IParams, IDcsHandler, IOscHandler, BgFlags, IRenderDimensions, IRenderService, Content, ExtFlags, Attributes, UnderlineStyle, ReadonlyColorSet };
|
||||
|
||||
/**
|
||||
* Plugin ctor options.
|
||||
*/
|
||||
export interface IImageAddonOptions {
|
||||
enableSizeReports: boolean;
|
||||
pixelLimit: number;
|
||||
storageLimit: number;
|
||||
showPlaceholder: boolean;
|
||||
sixelSupport: boolean;
|
||||
sixelScrolling: boolean;
|
||||
sixelPaletteLimit: number;
|
||||
sixelSizeLimit: number;
|
||||
iipSupport: boolean;
|
||||
iipSizeLimit: number;
|
||||
}
|
||||
|
||||
export interface IResetHandler {
|
||||
// attached to RIS and DECSTR
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub into private interfaces.
|
||||
* This should be kept in line with common libs.
|
||||
* Any change made here should be replayed in the accessors test case to
|
||||
* have a somewhat reliable testing against code changes in the core repo.
|
||||
*/
|
||||
|
||||
// overloaded IExtendedAttrs to hold image refs
|
||||
export interface IExtendedAttrsImage extends IExtendedAttrs {
|
||||
imageId: number;
|
||||
tileId: number;
|
||||
clone(): IExtendedAttrsImage;
|
||||
}
|
||||
|
||||
/* eslint-disable */
|
||||
export interface IBufferLineExt extends IBufferLine {
|
||||
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined};
|
||||
_data: Uint32Array;
|
||||
}
|
||||
|
||||
interface IInputHandlerExt extends IInputHandler {
|
||||
_parser: IEscapeSequenceParser;
|
||||
_curAttrData: AttributeData;
|
||||
_dirtyRowTracker: {
|
||||
markRangeDirty(y1: number, y2: number): void;
|
||||
markAllDirty(): void;
|
||||
markDirty(y: number): void;
|
||||
};
|
||||
onRequestReset(handler: () => void): IDisposable;
|
||||
}
|
||||
|
||||
export interface ICoreTerminalExt extends ITerminal {
|
||||
_themeService: IThemeService | undefined;
|
||||
_inputHandler: IInputHandlerExt;
|
||||
_renderService: IRenderService;
|
||||
_coreBrowserService: ICoreBrowserService | undefined;
|
||||
}
|
||||
|
||||
export interface ITerminalExt extends Terminal {
|
||||
_core: ICoreTerminalExt;
|
||||
}
|
||||
/* eslint-enable */
|
||||
|
||||
|
||||
/**
|
||||
* Some storage definitions.
|
||||
*/
|
||||
export interface ICellSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface IImageSpec {
|
||||
orig: HTMLCanvasElement | ImageBitmap | undefined;
|
||||
origCellSize: ICellSize;
|
||||
actual: HTMLCanvasElement | ImageBitmap | undefined;
|
||||
actualCellSize: ICellSize;
|
||||
marker: IMarker | undefined;
|
||||
tileCount: number;
|
||||
bufferType: 'alternate' | 'normal';
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2017",
|
||||
"module": "commonjs",
|
||||
"sourceMap": true,
|
||||
"outDir": "../out",
|
||||
"rootDir": ".",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"preserveWatchOutput": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
],
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"browser/*": [ "../../../src/browser/*" ],
|
||||
"common/*": [ "../../../src/common/*" ]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/browser" },
|
||||
{ "path": "../../../src/common" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { openTerminal, launchBrowser, pollFor } from '../../../out-test/api/TestUtils';
|
||||
import { Browser, Page } from '@playwright/test';
|
||||
import { IImageAddonOptions } from '../src/Types';
|
||||
import { FINALIZER, introducer, sixelEncode } from 'sixel';
|
||||
import { readFileSync } from 'fs';
|
||||
|
||||
const APP = 'http://127.0.0.1:3001/test';
|
||||
|
||||
let browser: Browser;
|
||||
let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare const ImageAddon: {
|
||||
new(options?: Partial<IImageAddonOptions>): any;
|
||||
};
|
||||
|
||||
interface ITestData {
|
||||
width: number;
|
||||
height: number;
|
||||
bytes: Uint8Array;
|
||||
palette: number[];
|
||||
sixel: string;
|
||||
}
|
||||
|
||||
interface IDimensions {
|
||||
cellWidth: number;
|
||||
cellHeight: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// image: 640 x 80, 512 color
|
||||
const TESTDATA: ITestData = (() => {
|
||||
const data8 = readFileSync('./addons/xterm-addon-image/fixture/palette.blob');
|
||||
const data32 = new Uint32Array(data8.buffer);
|
||||
const palette = new Set<number>();
|
||||
for (let i = 0; i < data32.length; ++i) palette.add(data32[i]);
|
||||
const sixel = sixelEncode(data8, 640, 80, [...palette]);
|
||||
return {
|
||||
width: 640,
|
||||
height: 80,
|
||||
bytes: data8,
|
||||
palette: [...palette],
|
||||
sixel
|
||||
};
|
||||
})();
|
||||
const SIXEL_SEQ_0 = introducer(0) + TESTDATA.sixel + FINALIZER;
|
||||
// const SIXEL_SEQ_1 = introducer(1) + TESTDATA.sixel + FINALIZER;
|
||||
// const SIXEL_SEQ_2 = introducer(2) + TESTDATA.sixel + FINALIZER;
|
||||
|
||||
// NOTE: the data is loaded as string for easier transport through playwright
|
||||
const TESTDATA_IIP: [string, [number, number]][] = [
|
||||
[readFileSync('./addons/xterm-addon-image/fixture/iip/palette.iip', { encoding: 'utf-8' }), [640, 80]],
|
||||
[readFileSync('./addons/xterm-addon-image/fixture/iip/spinfox.iip', { encoding: 'utf-8' }), [148, 148]],
|
||||
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_gif.iip', { encoding: 'utf-8' }), [72, 48]],
|
||||
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_jpg.iip', { encoding: 'utf-8' }), [72, 48]],
|
||||
[readFileSync('./addons/xterm-addon-image/fixture/iip/w3c_png.iip', { encoding: 'utf-8' }), [72, 48]]
|
||||
];
|
||||
|
||||
describe('ImageAddon', () => {
|
||||
before(async () => {
|
||||
browser = await launchBrowser();
|
||||
page = await (await browser.newContext()).newPage();
|
||||
await page.setViewportSize({ width, height });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await browser.close();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await page.goto(APP);
|
||||
await openTerminal(page);
|
||||
await page.evaluate(opts => {
|
||||
(window as any).imageAddon = new ImageAddon(opts.opts);
|
||||
(window as any).term.loadAddon((window as any).imageAddon);
|
||||
}, { opts: { sixelPaletteLimit: 512 } });
|
||||
});
|
||||
|
||||
it('test for private accessors', async () => {
|
||||
// terminal privates
|
||||
const accessors = [
|
||||
'_core',
|
||||
'_core._renderService',
|
||||
'_core._inputHandler',
|
||||
'_core._inputHandler._parser',
|
||||
'_core._inputHandler._curAttrData',
|
||||
'_core._inputHandler._dirtyRowTracker',
|
||||
'_core._themeService.colors',
|
||||
'_core._coreBrowserService'
|
||||
];
|
||||
for (const prop of accessors) {
|
||||
assert.equal(
|
||||
await page.evaluate('(() => { const v = window.term.' + prop + '; return v !== undefined && v !== null; })()'),
|
||||
true, `problem at ${prop}`
|
||||
);
|
||||
}
|
||||
// bufferline privates
|
||||
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
|
||||
assert.equal(await page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true);
|
||||
// inputhandler privates
|
||||
assert.equal(await page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), 'AttributeData');
|
||||
assert.equal(await page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
|
||||
});
|
||||
|
||||
describe('ctor options', () => {
|
||||
it('empty settings should load defaults', async () => {
|
||||
const DEFAULT_OPTIONS: IImageAddonOptions = {
|
||||
enableSizeReports: true,
|
||||
pixelLimit: 16777216,
|
||||
sixelSupport: true,
|
||||
sixelScrolling: true,
|
||||
sixelPaletteLimit: 512, // set to 512 to get example image working
|
||||
sixelSizeLimit: 25000000,
|
||||
storageLimit: 128,
|
||||
showPlaceholder: true,
|
||||
iipSupport: true,
|
||||
iipSizeLimit: 20000000
|
||||
};
|
||||
assert.deepEqual(await page.evaluate(`window.imageAddon._opts`), DEFAULT_OPTIONS);
|
||||
});
|
||||
it('custom settings should overload defaults', async () => {
|
||||
const customSettings: IImageAddonOptions = {
|
||||
enableSizeReports: false,
|
||||
pixelLimit: 5,
|
||||
sixelSupport: false,
|
||||
sixelScrolling: false,
|
||||
sixelPaletteLimit: 1024,
|
||||
sixelSizeLimit: 1000,
|
||||
storageLimit: 10,
|
||||
showPlaceholder: false,
|
||||
iipSupport: false,
|
||||
iipSizeLimit: 1000
|
||||
};
|
||||
await page.evaluate(opts => {
|
||||
(window as any).imageAddonCustom = new ImageAddon(opts.opts);
|
||||
(window as any).term.loadAddon((window as any).imageAddonCustom);
|
||||
}, { opts: customSettings });
|
||||
assert.deepEqual(await page.evaluate(`window.imageAddonCustom._opts`), customSettings);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrolling & cursor modes', () => {
|
||||
it('testdata default (scrolling with VT240 cursor pos)', async () => {
|
||||
const dim = await getDimensions();
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
|
||||
// moved to right by 10 cells
|
||||
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
|
||||
});
|
||||
it('write testdata noScrolling', async () => {
|
||||
await writeToTerminal('\x1b[?80h' + SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [0, 0]);
|
||||
// second draw does not change anything
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [0, 0]);
|
||||
});
|
||||
it('testdata cursor always at VT240 pos', async () => {
|
||||
const dim = await getDimensions();
|
||||
// offset 0
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [0, Math.floor(TESTDATA.height/dim.cellHeight)]);
|
||||
// moved to right by 10 cells
|
||||
await writeToTerminal('#'.repeat(10) + SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [10, Math.floor(TESTDATA.height/dim.cellHeight) * 2]);
|
||||
// moved by 30 cells (+10 prev)
|
||||
await writeToTerminal('#'.repeat(30) + SIXEL_SEQ_0);
|
||||
assert.deepEqual(await getCursor(), [10 + 30, Math.floor(TESTDATA.height/dim.cellHeight) * 3]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('image lifecycle & eviction', () => {
|
||||
it('delete image once scrolled off', async () => {
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
pollFor(page, 'window.imageAddon._storage._images.size', 1);
|
||||
// scroll to scrollback + rows - 1
|
||||
await page.evaluate(
|
||||
scrollback => new Promise(res => (window as any).term.write('\n'.repeat(scrollback), res)),
|
||||
(await getScrollbackPlusRows() - 1)
|
||||
);
|
||||
// wait here, as we have to make sure, that eviction did not yet occur
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
pollFor(page, 'window.imageAddon._storage._images.size', 1);
|
||||
// scroll one further should delete the image
|
||||
await page.evaluate(() => new Promise(res => (window as any).term.write('\n', res)));
|
||||
pollFor(page, 'window.imageAddon._storage._images.size', 0);
|
||||
});
|
||||
it('get storageUsage', async () => {
|
||||
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
|
||||
});
|
||||
it('get/set storageLimit', async () => {
|
||||
assert.equal(await page.evaluate('imageAddon.storageLimit'), 128);
|
||||
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
|
||||
assert.equal(await page.evaluate('imageAddon.storageLimit'), 1);
|
||||
});
|
||||
it('remove images by storage limit pressure', async () => {
|
||||
assert.equal(await page.evaluate('imageAddon.storageLimit = 1'), 1);
|
||||
// never go beyond storage limit
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const usage = await page.evaluate('imageAddon.storageUsage');
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
|
||||
assert.equal(usage as number < 1, true);
|
||||
});
|
||||
it('set storageLimit removes images synchronously', async () => {
|
||||
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
|
||||
const usage: number = await page.evaluate('imageAddon.storageUsage');
|
||||
const newUsage: number = await page.evaluate('imageAddon.storageLimit = 0.5; imageAddon.storageUsage');
|
||||
assert.equal(newUsage < usage, true);
|
||||
assert.equal(newUsage < 0.5, true);
|
||||
});
|
||||
it('clear alternate images on buffer change', async () => {
|
||||
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
|
||||
await writeToTerminal('\x1b[?1049h' + SIXEL_SEQ_0);
|
||||
assert.closeTo(await page.evaluate('imageAddon.storageUsage'), 640 * 80 * 4 / 1000000, 0.05);
|
||||
await writeToTerminal('\x1b[?1049l');
|
||||
assert.equal(await page.evaluate('imageAddon.storageUsage'), 0);
|
||||
});
|
||||
it('evict tiles by in-place overwrites (only full overwrite tested)', async () => {
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
|
||||
let usage = await page.evaluate('imageAddon.storageUsage');
|
||||
while (usage === 0) {
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
usage = await page.evaluate('imageAddon.storageUsage');
|
||||
}
|
||||
await writeToTerminal('\x1b[H' + SIXEL_SEQ_0 + '\x1b[100;100H');
|
||||
await new Promise(r => setTimeout(r, 200)); // wait some time and re-check
|
||||
assert.equal(await page.evaluate('imageAddon.storageUsage'), usage);
|
||||
});
|
||||
it('manual eviction on alternate buffer must not miss images', async () => {
|
||||
await writeToTerminal('\x1b[?1049h');
|
||||
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const usage: number = await page.evaluate('imageAddon.storageUsage');
|
||||
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
|
||||
await writeToTerminal(SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0 + SIXEL_SEQ_0);
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
const newUsage: number = await page.evaluate('imageAddon.storageUsage');
|
||||
assert.equal(newUsage, usage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IIP support - testimages', () => {
|
||||
it('palette.png', async () => {
|
||||
await writeToTerminal(TESTDATA_IIP[0][0]);
|
||||
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[0][1]);
|
||||
});
|
||||
it('spinfox.png', async () => {
|
||||
await writeToTerminal(TESTDATA_IIP[1][0]);
|
||||
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[1][1]);
|
||||
});
|
||||
it('w3c gif', async () => {
|
||||
await writeToTerminal(TESTDATA_IIP[2][0]);
|
||||
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[2][1]);
|
||||
});
|
||||
it('w3c jpeg', async () => {
|
||||
await writeToTerminal(TESTDATA_IIP[3][0]);
|
||||
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[3][1]);
|
||||
});
|
||||
it('w3c png', async () => {
|
||||
await writeToTerminal(TESTDATA_IIP[4][0]);
|
||||
assert.deepEqual(await getOrigSize(1), TESTDATA_IIP[4][1]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* terminal access helpers.
|
||||
*/
|
||||
async function getDimensions(): Promise<IDimensions> {
|
||||
const dimensions: any = await page.evaluate(`term._core._renderService.dimensions`);
|
||||
return {
|
||||
cellWidth: Math.round(dimensions.css.cell.width),
|
||||
cellHeight: Math.round(dimensions.css.cell.height),
|
||||
width: Math.round(dimensions.css.canvas.width),
|
||||
height: Math.round(dimensions.css.canvas.height)
|
||||
};
|
||||
}
|
||||
|
||||
async function getCursor(): Promise<[number, number]> {
|
||||
return page.evaluate('[window.term.buffer.active.cursorX, window.term.buffer.active.cursorY]');
|
||||
}
|
||||
|
||||
async function getImageStorageLength(): Promise<number> {
|
||||
return page.evaluate('window.imageAddon._storage._images.size');
|
||||
}
|
||||
|
||||
async function getScrollbackPlusRows(): Promise<number> {
|
||||
return page.evaluate('window.term.options.scrollback + window.term.rows');
|
||||
}
|
||||
|
||||
async function writeToTerminal(d: string): Promise<any> {
|
||||
return page.evaluate(data => new Promise(res => (window as any).term.write(data, res)), d);
|
||||
}
|
||||
|
||||
async function getOrigSize(id: number): Promise<[number, number]> {
|
||||
return page.evaluate<any>(`[
|
||||
window.imageAddon._storage._images.get(${id}).orig.width,
|
||||
window.imageAddon._storage._images.get(${id}).orig.height
|
||||
]`);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2017",
|
||||
"rootDir": ".",
|
||||
"outDir": "../out-test",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"strict": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"browser/*": [ "../../../src/browser/*" ],
|
||||
"common/*": [ "../../../src/common/*" ]
|
||||
},
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha",
|
||||
"../../../node_modules/@types/node",
|
||||
"../../../out-test/api/TestUtils"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/browser" },
|
||||
{ "path": "../../../src/common" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{ "path": "./src" },
|
||||
{ "path": "./test" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
|
||||
declare module 'xterm-addon-image' {
|
||||
export interface IImageAddonOptions {
|
||||
/**
|
||||
* Enable size reports in windowOptions:
|
||||
* - getWinSizePixels (CSI 14 t)
|
||||
* - getCellSizePixels (CSI 16 t)
|
||||
* - getWinSizeChars (CSI 18 t)
|
||||
*
|
||||
* If `true` (default), the reports will be activated during addon loading.
|
||||
* If `false`, no settings will be touched. Use `false`, if you have high
|
||||
* security constraints and/or deal with windowOptions by other means.
|
||||
* On addon disposal, the settings will not change.
|
||||
*/
|
||||
enableSizeReports?: boolean;
|
||||
|
||||
/**
|
||||
* Maximum pixels a single image may hold. Images exceeding this number will
|
||||
* be discarded during processing with no changes to the terminal buffer
|
||||
* (no cursor advance, no placeholder).
|
||||
* This setting is mainly used to restrict images sizes during initial decoding
|
||||
* including the final canvas creation.
|
||||
*
|
||||
* Note: The image worker decoder may hold additional memory up to
|
||||
* `pixelLimit` * 4 bytes permanently, plus the same amount on top temporarily
|
||||
* for pixel transfers, which should be taken into account under memory pressure conditions.
|
||||
*
|
||||
* Note: Browsers restrict allowed canvas dimensions further. We dont reflect those
|
||||
* limits here, thus the construction of an oddly shaped image having most pixels
|
||||
* in one dimension still can fail.
|
||||
*
|
||||
* Note: `storageLimit` bytes are calculated from images by multiplying the pixels with 4
|
||||
* (4 channels with one byte, images are stored as RGBA8888).
|
||||
*
|
||||
* Default is 2^16 (4096 x 4096 pixels).
|
||||
*/
|
||||
pixelLimit?: number;
|
||||
|
||||
/**
|
||||
* Storage limit in MB.
|
||||
* The storage implements a FIFO cache removing old images, when the limit gets hit.
|
||||
* Also exposed as addon property for runtime adjustments.
|
||||
* Default is 128 MB.
|
||||
*/
|
||||
storageLimit?: number;
|
||||
|
||||
/**
|
||||
* Whether to show a placeholder for images removed from cache, default is true.
|
||||
*/
|
||||
showPlaceholder?: boolean;
|
||||
|
||||
/**
|
||||
* SIXEL settings
|
||||
*/
|
||||
|
||||
/** Whether SIXEL is enabled (default is true). */
|
||||
sixelSupport?: boolean;
|
||||
/** Whether SIXEL scrolling is enabled (default is true). Same as DECSET 80. */
|
||||
sixelScrolling?: boolean;
|
||||
/** Palette color limit (default 256). */
|
||||
sixelPaletteLimit?: number;
|
||||
/** SIXEL image size limit in bytes (default 25000000 bytes). */
|
||||
sixelSizeLimit?: number;
|
||||
|
||||
/**
|
||||
* IIP settings (iTerm image protocol)
|
||||
*/
|
||||
|
||||
/** Whether iTerm image protocol style is enabled (default is true). */
|
||||
iipSupport?: boolean;
|
||||
/** IIP sequence size limit (default 20000000 bytes). */
|
||||
iipSizeLimit?: number;
|
||||
}
|
||||
|
||||
export class ImageAddon implements ITerminalAddon {
|
||||
constructor(options?: IImageAddonOptions);
|
||||
public activate(terminal: Terminal): void;
|
||||
public dispose(): void;
|
||||
|
||||
/**
|
||||
* Reset the image addon.
|
||||
*
|
||||
* This resets all runtime options to default values (as given to the ctor)
|
||||
* and resets the image storage.
|
||||
*/
|
||||
public reset(): void;
|
||||
|
||||
/**
|
||||
* Getter/Setter for the storageLimit in MB.
|
||||
* Synchronously deletes images if the stored data exceeds the new value.
|
||||
*/
|
||||
public storageLimit: number;
|
||||
|
||||
/**
|
||||
* Current memory usage of the stored images in MB.
|
||||
*/
|
||||
public readonly storageUsage: number;
|
||||
|
||||
/**
|
||||
* Getter/Setter whether the placeholder should be shown.
|
||||
*/
|
||||
public showPlaceholder: boolean;
|
||||
|
||||
/**
|
||||
* Get original image canvas at buffer position.
|
||||
*/
|
||||
public getImageAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
||||
|
||||
/**
|
||||
* Extract single tile canvas at buffer position.
|
||||
*/
|
||||
public extractTileAtBufferCell(x: number, y: number): HTMLCanvasElement | undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2020 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const addonName = 'ImageAddon';
|
||||
const mainFile = 'xterm-addon-image.js';
|
||||
|
||||
const addon = {
|
||||
entry: `./out/${addonName}.js`,
|
||||
devtool: 'source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
output: {
|
||||
filename: mainFile,
|
||||
path: path.resolve('./lib'),
|
||||
library: addonName,
|
||||
libraryTarget: 'umd'
|
||||
},
|
||||
mode: 'production'
|
||||
};
|
||||
|
||||
module.exports = [addon];
|
||||
@@ -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/
|
||||
|
||||