Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a512d1abd | ||
|
|
6ca8dc54ad | ||
|
|
cff944fe8a | ||
|
|
72dea6279e | ||
|
|
37ecb9a6b5 | ||
|
|
fb867a914a | ||
|
|
8ef3c10844 | ||
|
|
10fe249877 | ||
|
|
5143f904ee | ||
|
|
768452a519 | ||
|
|
547bda6e4f | ||
|
|
6a281bd90e | ||
|
|
1c4e0111cb | ||
|
|
8d1771ef64 | ||
|
|
fb54bb0b6c | ||
|
|
546bcb5133 | ||
|
|
b1a72b2e83 | ||
|
|
dce6e31c89 | ||
|
|
cddc888065 | ||
|
|
06aa2c86df | ||
|
|
c51a0b7454 | ||
|
|
cae42772e8 | ||
|
|
09a0dd658d | ||
|
|
bbd1a1dbe2 | ||
|
|
f1e563ad5f | ||
|
|
8e015ebf87 | ||
|
|
3c88c767cd | ||
|
|
99df13b085 | ||
|
|
f03d870bcd | ||
|
|
856666d5bd | ||
|
|
1940bbec75 | ||
|
|
7b7fb73611 | ||
|
|
b97baa7ff1 | ||
|
|
a7bf3b22c1 | ||
|
|
442c1583e7 | ||
|
|
217acef6b2 | ||
|
|
0eed746638 | ||
|
|
1b4f0606ca | ||
|
|
3c4d567be9 | ||
|
|
82d50a4f89 | ||
|
|
379c382fe7 | ||
|
|
559026bbf3 | ||
|
|
08e67a3a0b | ||
|
|
1a82e0d421 | ||
|
|
2e03680128 | ||
|
|
94a9ce8566 | ||
|
|
6e351dd4ac | ||
|
|
53604e427b | ||
|
|
1b52c97fd0 | ||
|
|
dbcb43f1d3 | ||
|
|
d7c8d063b6 | ||
|
|
de41c9f306 | ||
|
|
8733d2cfa2 | ||
|
|
b3737d6ff0 | ||
|
|
56a6a010ab | ||
|
|
341d9f0597 | ||
|
|
a4505ed724 | ||
|
|
b843e0fc8e | ||
|
|
c0fac5e5f1 | ||
|
|
78a945ae9e | ||
|
|
9b62849bb8 | ||
|
|
66b426311e | ||
|
|
7bee001868 | ||
|
|
22f1ce4a11 | ||
|
|
805823df7a | ||
|
|
7c5ad6b7a9 | ||
|
|
0904078bf4 | ||
|
|
9f995c6ff8 | ||
|
|
427b4f6c6d | ||
|
|
81a4d860c5 | ||
|
|
7ac95fab28 | ||
|
|
1f15c47a43 | ||
|
|
88ba66d00b | ||
|
|
2592e1e84d | ||
|
|
97750f672f | ||
|
|
54608bf38d | ||
|
|
b4226bc0f5 | ||
|
|
1943636f02 | ||
|
|
d218bff2c4 | ||
|
|
fb2c39cb57 | ||
|
|
b595940bcf | ||
|
|
49eab6b1d9 | ||
|
|
2861a4dd39 | ||
|
|
b4fa7c6c06 | ||
|
|
58bb7fc73e | ||
|
|
b16724f06a | ||
|
|
5630239a6d | ||
|
|
13be985b36 | ||
|
|
a5646a58d5 | ||
|
|
b0407e20cf | ||
|
|
8a1790d6d5 | ||
|
|
86074c9210 | ||
|
|
2f074b87f3 | ||
|
|
6214f4c2e6 | ||
|
|
390e4f30c8 | ||
|
|
4036ab7055 | ||
|
|
139bf361b4 | ||
|
|
c9f15454e4 | ||
|
|
b6a09d67e2 | ||
|
|
1770bb1b65 | ||
|
|
52549f0a7a | ||
|
|
0d7cc9e9fd | ||
|
|
e8da73799f | ||
|
|
588da4f3b8 | ||
|
|
4beb7d0595 | ||
|
|
4c275a287f | ||
|
|
5ed2b013d5 | ||
|
|
19bdcab93e | ||
|
|
45e7efce67 | ||
|
|
fbc51ae691 | ||
|
|
92eb78230b | ||
|
|
31925596f1 | ||
|
|
88b0b4a5c7 | ||
|
|
c8b2783b85 | ||
|
|
2744549c0f | ||
|
|
1c522e16a4 | ||
|
|
4b2e52f13c | ||
|
|
bbbde9261f | ||
|
|
eac8124320 | ||
|
|
64534e1b7a | ||
|
|
71ec216161 | ||
|
|
2df591c5f7 | ||
|
|
d95f0177c8 | ||
|
|
b4b128709f | ||
|
|
0fd04aa76d | ||
|
|
4abb4c674f | ||
|
|
485fb455d6 | ||
|
|
765a8b21a3 | ||
|
|
dcc940c32e | ||
|
|
059acb2da6 | ||
|
|
a1f87809b2 | ||
|
|
2fb9702a3a | ||
|
|
5196ac5c8f | ||
|
|
9db2178d05 | ||
|
|
8c6d16486d | ||
|
|
cef869a206 | ||
|
|
efbcd41fab | ||
|
|
bb9728ccd1 | ||
|
|
905285efda | ||
|
|
e1d19e00a5 | ||
|
|
add7f8133e | ||
|
|
33100818d7 | ||
|
|
974b275f6e | ||
|
|
ea2d775595 | ||
|
|
f741d42165 | ||
|
|
6945e881a8 | ||
|
|
804ff187fa | ||
|
|
de1304d19e | ||
|
|
9fff1e6e44 | ||
|
|
0d0166b4bc | ||
|
|
6c5fb36c36 | ||
|
|
1c880f5199 | ||
|
|
4681b319ca | ||
|
|
78a6aac409 | ||
|
|
20a26a6589 | ||
|
|
a4e09d0ad8 | ||
|
|
dfd5385602 | ||
|
|
7dab4601d2 | ||
|
|
81e0718a74 | ||
|
|
6e12d94f11 | ||
|
|
92535e539f | ||
|
|
4cc51ca08f | ||
|
|
63790f8854 | ||
|
|
45605db3c5 | ||
|
|
c80453f602 | ||
|
|
eb4277e7bd | ||
|
|
4245283e05 | ||
|
|
41c62288f0 | ||
|
|
c3aa63cffa | ||
|
|
1b940de0bf | ||
|
|
023c778c4d | ||
|
|
6b5fc260ca | ||
|
|
d3e075f37b | ||
|
|
194b810ad5 | ||
|
|
7cbaacde40 | ||
|
|
34017f32fb | ||
|
|
57e2adc042 | ||
|
|
e9390d0d91 | ||
|
|
f6c4aee928 | ||
|
|
a673b110f5 | ||
|
|
cfde4d634f | ||
|
|
4e4110aeb4 | ||
|
|
4149aa0945 | ||
|
|
810cc1d47e | ||
|
|
c08062420f | ||
|
|
bde7bdc11d | ||
|
|
a3e933d98b | ||
|
|
543b82567f | ||
|
|
cfc94b332a | ||
|
|
f391cf6d43 | ||
|
|
da2687174f | ||
|
|
a8ef1d49a1 | ||
|
|
feb75a7f4d | ||
|
|
27fd011e35 | ||
|
|
03f1336042 | ||
|
|
fbf7d7adb5 | ||
|
|
da041f43ee | ||
|
|
a6f57262f6 | ||
|
|
aa5ec44c27 | ||
|
|
446549a043 | ||
|
|
ef21ed901f | ||
|
|
aaadefa548 | ||
|
|
0e66d608a1 | ||
|
|
1618092b91 | ||
|
|
2f9439b690 | ||
|
|
66c8e33b47 | ||
|
|
9048ded5eb | ||
|
|
17575e1d7b | ||
|
|
49dc046b9f | ||
|
|
27d89c2311 | ||
|
|
871400f407 | ||
|
|
a1229c78a2 | ||
|
|
457060f89d | ||
|
|
cc14bc4515 | ||
|
|
bdb25c2dbe | ||
|
|
32c616512d | ||
|
|
16bd8de78e | ||
|
|
3ad0ef1ac4 | ||
|
|
cb8917557d | ||
|
|
8183d1d221 | ||
|
|
9ab0cd648e | ||
|
|
249e1152f4 | ||
|
|
d41b747aa1 | ||
|
|
f69fcb971c | ||
|
|
bfa6f656ef | ||
|
|
24b932d67a | ||
|
|
9e536ca9c3 | ||
|
|
17c4d097bb | ||
|
|
1b5756c1b1 | ||
|
|
a003034b02 | ||
|
|
bddff604c2 | ||
|
|
b2c123e0e2 | ||
|
|
be82ffc01d | ||
|
|
efcca3fb3e | ||
|
|
b91de01a4d | ||
|
|
6b1c1e0751 | ||
|
|
8985326dd8 | ||
|
|
c609f5cca0 | ||
|
|
3b15fb6211 | ||
|
|
a7eb112d36 | ||
|
|
cc43fe5191 | ||
|
|
cfced0caf1 | ||
|
|
fc18e6e774 | ||
|
|
1d57ac5c63 | ||
|
|
b55d699451 | ||
|
|
4241704261 | ||
|
|
b50d21434e | ||
|
|
c3fd1c2c40 | ||
|
|
fe4528c9be | ||
|
|
b0868967cb | ||
|
|
1aac37e14f | ||
|
|
851c2e39af | ||
|
|
d2387342ce | ||
|
|
1792eaf05f | ||
|
|
36bf3ebff3 | ||
|
|
681ba4887f | ||
|
|
b7f28c56a9 | ||
|
|
575cd8d28b | ||
|
|
3d440e21bd | ||
|
|
a10113543b | ||
|
|
50369713de | ||
|
|
2f2154c13b | ||
|
|
056206d7a4 | ||
|
|
57ab29bb85 | ||
|
|
4e69307074 | ||
|
|
608a5e2329 | ||
|
|
5126481de2 | ||
|
|
fa168bd4c0 | ||
|
|
f159a862e4 | ||
|
|
9f5ff468e1 | ||
|
|
65e07dcebc | ||
|
|
cc83b9a4ca | ||
|
|
14872e091e | ||
|
|
3d3f58b6ff | ||
|
|
b99158f8f0 | ||
|
|
2d74505208 | ||
|
|
534b03b99f | ||
|
|
4af4d8dcc6 | ||
|
|
298c5b2041 | ||
|
|
328e97336b | ||
|
|
2e10a7f097 | ||
|
|
1798cab222 | ||
|
|
ebd27de4e1 | ||
|
|
beeff131a9 | ||
|
|
408240dd97 | ||
|
|
71eb897708 | ||
|
|
02aadf7c3e | ||
|
|
98e8c6ac73 | ||
|
|
34933d2c76 | ||
|
|
a0c34b19df | ||
|
|
48ba5e5856 | ||
|
|
47183b2939 | ||
|
|
9b9062aea4 | ||
|
|
cdf02d54da | ||
|
|
413e884efb | ||
|
|
070d194c6c | ||
|
|
e54755b074 | ||
|
|
db608e33fb | ||
|
|
b543842fc0 | ||
|
|
97be015234 | ||
|
|
a656409bb5 | ||
|
|
ea45b04105 | ||
|
|
66a6ecea14 | ||
|
|
f23af3661b | ||
|
|
2f233f2389 | ||
|
|
393a8b3a86 | ||
|
|
c64115e0a0 | ||
|
|
427c472f6f | ||
|
|
2e02c37e52 | ||
|
|
71ee3c7a33 | ||
|
|
009add2b4a | ||
|
|
fb4b662756 | ||
|
|
c1882eb1ff | ||
|
|
9963c58511 | ||
|
|
7b52b8ee45 | ||
|
|
c03cb2b016 | ||
|
|
930e5c4280 | ||
|
|
630d213bd0 | ||
|
|
82b6dd1ac8 | ||
|
|
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 | ||
|
|
ceef536b79 | ||
|
|
7fa56fde0b | ||
|
|
7bd9f8ee38 | ||
|
|
2dd4dfda80 | ||
|
|
f7ac0d878b | ||
|
|
fe2723b585 | ||
|
|
7fecec6599 | ||
|
|
44e19775f5 | ||
|
|
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 | ||
|
|
42d6c2bd6b | ||
|
|
810e62665f | ||
|
|
bec1642123 | ||
|
|
08f7b4c53c | ||
|
|
d15abd7f28 | ||
|
|
d367eb41d2 | ||
|
|
7c164efdcc | ||
|
|
bd76cdbd20 | ||
|
|
dc6dd6d2a8 | ||
|
|
40e51aba6b | ||
|
|
f9dc26d413 | ||
|
|
2fe082beea | ||
|
|
2835976b81 | ||
|
|
1968e8ced4 | ||
|
|
1965daff50 | ||
|
|
f4881872f7 | ||
|
|
f1f0191d98 | ||
|
|
6ace416224 | ||
|
|
52c87bc8b3 | ||
|
|
edfacbb5c6 | ||
|
|
564371eae7 | ||
|
|
e09ed98cf1 | ||
|
|
2b0106f7e2 | ||
|
|
0e3db591c0 | ||
|
|
224902af9f | ||
|
|
131f578367 | ||
|
|
6b794285c6 | ||
|
|
e78f85bf65 | ||
|
|
9be0b6d918 | ||
|
|
3aa31111de | ||
|
|
81baad6acd | ||
|
|
25ef47a78f | ||
|
|
7d7d593fee | ||
|
|
766ffde4ca | ||
|
|
4534b56c1f | ||
|
|
ee0da5e4c1 | ||
|
|
e5d60c0895 | ||
|
|
5ccf4d3311 | ||
|
|
1f3143cfab | ||
|
|
f5d128b0d5 | ||
|
|
2268ae3967 | ||
|
|
921da1a2ea | ||
|
|
ebd4092bee | ||
|
|
b6fd7a2b93 | ||
|
|
210ba8109c | ||
|
|
25afc19895 | ||
|
|
68ded1a5b4 | ||
|
|
317ccdac66 | ||
|
|
43a3251bee | ||
|
|
94ab56542f | ||
|
|
92f1881dfe | ||
|
|
ec4c018f73 | ||
|
|
2c25dd1970 | ||
|
|
06fbe69b09 | ||
|
|
4bd154e7dc | ||
|
|
516756fad8 | ||
|
|
2b3cfb39a1 | ||
|
|
e9cbb3e547 | ||
|
|
1744801867 | ||
|
|
64cb9a3c62 | ||
|
|
0e947788e0 | ||
|
|
60b59db94f | ||
|
|
6d7032a906 | ||
|
|
b452fc9e85 | ||
|
|
dac4dec0ad | ||
|
|
eae79e93f3 | ||
|
|
ee33765932 | ||
|
|
8af83fef85 | ||
|
|
5f0647f0ab | ||
|
|
fe7f17f422 | ||
|
|
e5a2ebda0e | ||
|
|
2204375788 | ||
|
|
c81dfee43c | ||
|
|
cb175e513d | ||
|
|
32da1d81d5 | ||
|
|
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 | ||
|
|
e21e52569c | ||
|
|
7b7dbd36d6 | ||
|
|
d03b5c9481 | ||
|
|
93e060aeb2 | ||
|
|
00fd2b8503 | ||
|
|
a84351ad0c | ||
|
|
25a78c77f2 | ||
|
|
a35fa611ff | ||
|
|
0b54c9a5d6 | ||
|
|
21fd545381 | ||
|
|
6c1eb97a18 | ||
|
|
2070cc45d4 | ||
|
|
f1bc95622d | ||
|
|
3b3b23a2fd | ||
|
|
e3d3cdb9d2 | ||
|
|
7b874473d3 | ||
|
|
9d7b9293eb | ||
|
|
957e3e0f08 | ||
|
|
5994e25841 | ||
|
|
2f0a7eee8d | ||
|
|
1a3e14a24c | ||
|
|
a6b67fb8a6 | ||
|
|
843bdc4d55 | ||
|
|
2aee19f0e1 | ||
|
|
9f3be5e28d | ||
|
|
f69f803f0f | ||
|
|
2eca26b210 | ||
|
|
2ca0ccbe0c | ||
|
|
cf243adfe3 | ||
|
|
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 | ||
|
|
aac5d4704e | ||
|
|
fb2c6801d4 | ||
|
|
bb5d61fb14 | ||
|
|
3b3012353f | ||
|
|
adad53d526 | ||
|
|
9b21786062 | ||
|
|
1b4460e37d | ||
|
|
482dad3036 | ||
|
|
7276027861 | ||
|
|
62055522fb | ||
|
|
0366aa2217 | ||
|
|
2b79b7b29f | ||
|
|
3bfe5d86c0 | ||
|
|
02243173c2 | ||
|
|
a84a95055d | ||
|
|
b8bf35be17 | ||
|
|
b9a4760b44 | ||
|
|
3ba579f680 | ||
|
|
302da18c81 | ||
|
|
4fbc623706 | ||
|
|
b9abbc0a85 | ||
|
|
f1035405e6 | ||
|
|
720297946b | ||
|
|
988b3b1ab2 | ||
|
|
88bfc6bb2b | ||
|
|
a0dc88165f | ||
|
|
dccfc11b4f | ||
|
|
42042155d4 | ||
|
|
6b2459317b | ||
|
|
08a0914a78 | ||
|
|
4f41070b45 | ||
|
|
d2031955ec | ||
|
|
7baff4e249 | ||
|
|
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 | ||
|
|
556823328f | ||
|
|
dc6818de2b | ||
|
|
693f6b2d14 | ||
|
|
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 | ||
|
|
cf403bc45c | ||
|
|
fa89e1776b | ||
|
|
1a8c7dad5f | ||
|
|
83a77b6024 | ||
|
|
8b422442b7 | ||
|
|
02ce3eff87 | ||
|
|
518b87f01d | ||
|
|
e87fa1600d | ||
|
|
6188c07bf7 | ||
|
|
325d133a75 | ||
|
|
b662824b81 | ||
|
|
ada5bfe048 | ||
|
|
f270127ce5 | ||
|
|
7460782494 | ||
|
|
048010c219 | ||
|
|
cdec1e5b03 | ||
|
|
eb56356133 | ||
|
|
2bfd95fe54 | ||
|
|
e9b0593298 | ||
|
|
d5c47ec531 | ||
|
|
5a145fcffe | ||
|
|
539fed4daa | ||
|
|
3003185676 | ||
|
|
ebfa2018dd | ||
|
|
68abf10c25 | ||
|
|
284cde8b1b | ||
|
|
466501c62f | ||
|
|
9c34bb4160 | ||
|
|
8e730cdc61 | ||
|
|
41760dfb09 | ||
|
|
67e968926c | ||
|
|
a0493a604c | ||
|
|
07e8f86bd1 | ||
|
|
1dbc1bf639 | ||
|
|
6e8c59f0db | ||
|
|
ec227c757b | ||
|
|
2f7fe1189c | ||
|
|
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 | ||
|
|
25eb7d6472 | ||
|
|
6225cc2b9c |
@@ -1,23 +0,0 @@
|
|||||||
FROM node:14-buster
|
|
||||||
|
|
||||||
# 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
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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",
|
"name": "xterm.js",
|
||||||
"dockerFile": "Dockerfile",
|
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster",
|
||||||
"appPort": 3000,
|
"features": {
|
||||||
"extensions": [
|
"ghcr.io/devcontainers/features/node:1": {} // yarn
|
||||||
"dbaeumer.vscode-eslint",
|
},
|
||||||
"editorconfig.editorconfig",
|
"forwardPorts": [
|
||||||
"hbenl.vscode-mocha-test-adapter"
|
3000
|
||||||
]
|
],
|
||||||
|
"postCreateCommand": "yarn install",
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"hbenl.vscode-mocha-test-adapter"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,57 @@
|
|||||||
{
|
{
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true,
|
"browser": true,
|
||||||
"es6": true,
|
"es2021": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": [
|
"project": [
|
||||||
|
"demo/tsconfig.json",
|
||||||
"src/browser/tsconfig.json",
|
"src/browser/tsconfig.json",
|
||||||
"src/common/tsconfig.json",
|
"src/common/tsconfig.json",
|
||||||
"src/headless/tsconfig.json",
|
"src/headless/tsconfig.json",
|
||||||
"test/api/tsconfig.json",
|
"test/api/tsconfig.json",
|
||||||
"test/benchmark/tsconfig.json",
|
"test/benchmark/tsconfig.json",
|
||||||
"addons/xterm-addon-attach/src/tsconfig.json",
|
"test/playwright/tsconfig.json",
|
||||||
"addons/xterm-addon-attach/test/tsconfig.json",
|
"addons/addon-attach/src/tsconfig.json",
|
||||||
"addons/xterm-addon-canvas/src/tsconfig.json",
|
"addons/addon-attach/test/tsconfig.json",
|
||||||
"addons/xterm-addon-fit/src/tsconfig.json",
|
"addons/addon-canvas/src/tsconfig.json",
|
||||||
"addons/xterm-addon-fit/test/tsconfig.json",
|
"addons/addon-canvas/test/tsconfig.json",
|
||||||
"addons/xterm-addon-ligatures/src/tsconfig.json",
|
"addons/addon-fit/src/tsconfig.json",
|
||||||
"addons/xterm-addon-search/src/tsconfig.json",
|
"addons/addon-fit/test/tsconfig.json",
|
||||||
"addons/xterm-addon-search/test/tsconfig.json",
|
"addons/addon-image/src/tsconfig.json",
|
||||||
"addons/xterm-addon-serialize/src/tsconfig.json",
|
"addons/addon-image/test/tsconfig.json",
|
||||||
"addons/xterm-addon-serialize/test/tsconfig.json",
|
"addons/addon-ligatures/src/tsconfig.json",
|
||||||
"addons/xterm-addon-serialize/benchmark/tsconfig.json",
|
"addons/addon-search/src/tsconfig.json",
|
||||||
"addons/xterm-addon-unicode11/src/tsconfig.json",
|
"addons/addon-search/test/tsconfig.json",
|
||||||
"addons/xterm-addon-unicode11/test/tsconfig.json",
|
"addons/addon-serialize/src/tsconfig.json",
|
||||||
"addons/xterm-addon-web-links/src/tsconfig.json",
|
"addons/addon-serialize/test/tsconfig.json",
|
||||||
"addons/xterm-addon-web-links/test/tsconfig.json",
|
"addons/addon-serialize/benchmark/tsconfig.json",
|
||||||
"addons/xterm-addon-webgl/src/tsconfig.json",
|
"addons/addon-unicode11/src/tsconfig.json",
|
||||||
"addons/xterm-addon-webgl/test/tsconfig.json"
|
"addons/addon-unicode11/test/tsconfig.json",
|
||||||
|
"addons/addon-unicode-graphemes/src/tsconfig.json",
|
||||||
|
"addons/addon-unicode-graphemes/test/tsconfig.json",
|
||||||
|
"addons/addon-unicode-graphemes/benchmark/tsconfig.json",
|
||||||
|
"addons/addon-web-links/src/tsconfig.json",
|
||||||
|
"addons/addon-web-links/test/tsconfig.json",
|
||||||
|
"addons/addon-webgl/src/tsconfig.json",
|
||||||
|
"addons/addon-webgl/test/tsconfig.json"
|
||||||
],
|
],
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"ignorePatterns": [
|
"ignorePatterns": [
|
||||||
|
"addons/*/src/third-party/*.ts",
|
||||||
|
"out/*",
|
||||||
|
"out-test/*",
|
||||||
|
"**/inwasm-sdks/*",
|
||||||
"**/typings/*.d.ts",
|
"**/typings/*.d.ts",
|
||||||
"**/node_modules",
|
"**/node_modules",
|
||||||
"**/*.js"
|
"**/*.js"
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@typescript-eslint"
|
"@typescript-eslint",
|
||||||
|
"jsdoc"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-extra-semi": "error",
|
"no-extra-semi": "error",
|
||||||
@@ -85,7 +98,12 @@
|
|||||||
],
|
],
|
||||||
"@typescript-eslint/naming-convention": [
|
"@typescript-eslint/naming-convention": [
|
||||||
"warn",
|
"warn",
|
||||||
{ "selector": "default", "format": ["camelCase"] },
|
{ "selector": "default", "format": ["camelCase"],
|
||||||
|
"filter": {
|
||||||
|
"regex": "^[a-z]",
|
||||||
|
"match": true
|
||||||
|
}
|
||||||
|
},
|
||||||
// variableLike
|
// variableLike
|
||||||
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
|
{ "selector": "variable", "format": ["camelCase", "UPPER_CASE"] },
|
||||||
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
|
{ "selector": "variable", "filter": "^I.+Service$", "format": ["PascalCase"], "prefix": ["I"] },
|
||||||
@@ -94,12 +112,34 @@
|
|||||||
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
{ "selector": "memberLike", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require" },
|
||||||
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
||||||
// memberLike - Allow enum-like objects to use UPPER_CASE
|
// memberLike - Allow enum-like objects to use UPPER_CASE
|
||||||
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"],
|
||||||
{ "selector": "method", "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
|
// typeLike
|
||||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
{ "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/prefer-namespace-keyword": "warn",
|
||||||
"@typescript-eslint/type-annotation-spacing": "warn",
|
"@typescript-eslint/type-annotation-spacing": "warn",
|
||||||
"@typescript-eslint/quotes": [
|
"@typescript-eslint/quotes": [
|
||||||
@@ -128,7 +168,20 @@
|
|||||||
"warn",
|
"warn",
|
||||||
"always"
|
"always"
|
||||||
],
|
],
|
||||||
|
"jsdoc/check-alignment": 1,
|
||||||
|
"jsdoc/check-param-names": 1,
|
||||||
|
"jsdoc/no-multi-asterisks": 1,
|
||||||
"keyword-spacing": "warn",
|
"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",
|
"new-parens": "warn",
|
||||||
"no-duplicate-imports": "warn",
|
"no-duplicate-imports": "warn",
|
||||||
"no-else-return": [
|
"no-else-return": [
|
||||||
@@ -172,6 +225,10 @@
|
|||||||
],
|
],
|
||||||
"no-trailing-spaces": "warn",
|
"no-trailing-spaces": "warn",
|
||||||
"no-unsafe-finally": "warn",
|
"no-unsafe-finally": "warn",
|
||||||
|
"no-unused-vars": ["warn", {
|
||||||
|
"vars": "all",
|
||||||
|
"args": "none"
|
||||||
|
}],
|
||||||
"no-var": "warn",
|
"no-var": "warn",
|
||||||
"one-var": [
|
"one-var": [
|
||||||
"warn",
|
"warn",
|
||||||
@@ -193,9 +250,14 @@
|
|||||||
},
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["**/*.test.ts"],
|
"files": [
|
||||||
|
"**/*.api.ts",
|
||||||
|
"**/*.test.ts"
|
||||||
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"object-curly-spacing": "off"
|
"object-curly-spacing": "off",
|
||||||
|
"max-len": "off",
|
||||||
|
"no-unused-vars": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": 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
|
blank_issues_enabled: false
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Question
|
- name: FAQ
|
||||||
url: https://stackoverflow.com/questions/tagged/xtermjs
|
url: https://github.com/xtermjs/xterm.js/wiki/FAQ
|
||||||
about: Please ask and answer questions here.
|
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,312 @@
|
|||||||
|
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/addon-attach/out/* \
|
||||||
|
./addons/addon-attach/out-test/* \
|
||||||
|
./addons/addon-canvas/out/* \
|
||||||
|
./addons/addon-canvas/out-test/* \
|
||||||
|
./addons/addon-fit/out/* \
|
||||||
|
./addons/addon-fit/out-test/* \
|
||||||
|
./addons/addon-image/out/* \
|
||||||
|
./addons/addon-image/out-test/* \
|
||||||
|
./addons/addon-ligatures/out/* \
|
||||||
|
./addons/addon-ligatures/out-test/* \
|
||||||
|
./addons/addon-search/out/* \
|
||||||
|
./addons/addon-search/out-test/* \
|
||||||
|
./addons/addon-serialize/out/* \
|
||||||
|
./addons/addon-serialize/out-test/* \
|
||||||
|
./addons/addon-unicode11/out/* \
|
||||||
|
./addons/addon-unicode11/out-test/* \
|
||||||
|
./addons/addon-unicode-graphemes/out/* \
|
||||||
|
./addons/addon-unicode-graphemes/out-test/* \
|
||||||
|
./addons/addon-web-links/out/* \
|
||||||
|
./addons/addon-web-links/out-test/* \
|
||||||
|
./addons/addon-webgl/out/* \
|
||||||
|
./addons/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
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: --max_old_space_size=4096
|
||||||
|
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-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 (core) # Tests use 50% workers to reduce flakiness
|
||||||
|
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=core
|
||||||
|
- name: Integration tests (addon-canvas)
|
||||||
|
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-canvas
|
||||||
|
- name: Integration tests (addon-webgl)
|
||||||
|
run: yarn test-playwright-${{ matrix.browser }} --workers=50% --forbid-only --suite=addon-webgl
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
release-dry-run:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18]
|
||||||
|
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: Package headless
|
||||||
|
run: |
|
||||||
|
yarn package-headless
|
||||||
|
node ./bin/package_headless.js
|
||||||
|
- name: Publish to npm (dry run)
|
||||||
|
run: node ./bin/publish.js --dry
|
||||||
@@ -1,21 +1,9 @@
|
|||||||
# For most projects, this workflow file will not need changing; you simply need
|
|
||||||
# to commit it to your repository.
|
|
||||||
#
|
|
||||||
# You may wish to alter this file to override the set of languages analyzed,
|
|
||||||
# or to provide custom queries or build logic.
|
|
||||||
#
|
|
||||||
# ******** NOTE ********
|
|
||||||
# We have attempted to detect the languages in your repository. Please check
|
|
||||||
# the `language` matrix defined below to confirm you have the correct set of
|
|
||||||
# supported CodeQL languages.
|
|
||||||
#
|
|
||||||
name: "CodeQL"
|
name: "CodeQL"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
pull_request:
|
pull_request:
|
||||||
# The branches below must be a subset of the branches above
|
|
||||||
branches: [ "master" ]
|
branches: [ "master" ]
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '41 17 * * 0'
|
- cron: '41 17 * * 0'
|
||||||
@@ -33,40 +21,15 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: [ 'javascript' ]
|
language: [ 'javascript' ]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
|
|
||||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
||||||
# queries: security-extended,security-and-quality
|
|
||||||
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
||||||
|
|
||||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
|
||||||
|
|
||||||
# - run: |
|
|
||||||
# echo "Run, Build Application using script"
|
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
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
|
||||||
@@ -43,7 +43,14 @@
|
|||||||
{
|
{
|
||||||
"type": "chrome",
|
"type": "chrome",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Demo Client",
|
"name": "Demo Client (Chrome)",
|
||||||
|
"url": "http://127.0.0.1:3000",
|
||||||
|
"webRoot": "${workspaceFolder}/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "msedge",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Demo Client (Edge)",
|
||||||
"url": "http://127.0.0.1:3000",
|
"url": "http://127.0.0.1:3000",
|
||||||
"webRoot": "${workspaceFolder}/"
|
"webRoot": "${workspaceFolder}/"
|
||||||
},
|
},
|
||||||
@@ -52,11 +59,9 @@
|
|||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Demo Server",
|
"name": "Demo Server",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": [
|
"runtimeArgs": ["start"],
|
||||||
"run",
|
"stopOnEntry": true,
|
||||||
"start-debug"
|
"runtimeVersion": "16",
|
||||||
],
|
|
||||||
"port": 9229,
|
|
||||||
"serverReadyAction": {
|
"serverReadyAction": {
|
||||||
"action": "openExternally",
|
"action": "openExternally",
|
||||||
"pattern": "App listening to (http://.*?:[0-9]+)"
|
"pattern": "App listening to (http://.*?:[0-9]+)"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"files.associations": {
|
||||||
|
".eslintrc.json.typings": "jsonc"
|
||||||
|
},
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||||
"typescript.preferences.quoteStyle": "single",
|
"typescript.preferences.quoteStyle": "single",
|
||||||
"mochaExplorer.envPath": ".mocha.env",
|
"mochaExplorer.envPath": ".mocha.env",
|
||||||
|
|||||||
@@ -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
|
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||||
bottom of the pile.
|
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
|
## 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:
|
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://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.
|
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
|
## Features
|
||||||
@@ -51,7 +49,7 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
|||||||
The recommended way to load xterm.js is via the ES6 module syntax:
|
The recommended way to load xterm.js is via the ES6 module syntax:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Addons
|
### Addons
|
||||||
@@ -61,14 +59,14 @@ import { Terminal } from 'xterm';
|
|||||||
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
|
Addons are separate modules that extend the `Terminal` by building on the [xterm.js API](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts). To use an addon, you first need to install it in your project:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm i -S xterm-addon-web-links
|
npm i -S @xterm/addon-web-links
|
||||||
```
|
```
|
||||||
|
|
||||||
Then import the addon, instantiate it and call `Terminal.loadAddon`:
|
Then import the addon, instantiate it and call `Terminal.loadAddon`:
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
||||||
|
|
||||||
const terminal = new Terminal();
|
const terminal = new Terminal();
|
||||||
// Load WebLinksAddon on terminal, this is all that's needed to get web links
|
// Load WebLinksAddon on terminal, this is all that's needed to get web links
|
||||||
@@ -78,10 +76,15 @@ terminal.loadAddon(new WebLinksAddon());
|
|||||||
|
|
||||||
The xterm.js team maintains the following addons, but anyone can build them:
|
The xterm.js team maintains the following addons, but anyone can build them:
|
||||||
|
|
||||||
- [`xterm-addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-attach): Attaches to a server running a process via a websocket
|
- [`@xterm/addon-attach`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-attach): Attaches to a server running a process via a websocket
|
||||||
- [`xterm-addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-fit): Fits the terminal to the containing element
|
- [`@xterm/addon-canvas`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-canvas): Renders xterm.js using a `canvas` element's 2d context
|
||||||
- [`xterm-addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-search): Adds search functionality
|
- [`@xterm/addon-fit`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit): Fits the terminal to the containing element
|
||||||
- [`xterm-addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/xterm-addon-web-links): Adds web link detection and interaction
|
- [`@xterm/addon-image`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image): Adds image support
|
||||||
|
- [`@xterm/addon-search`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-search): Adds search functionality
|
||||||
|
- [`@xterm/addon-serialize`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-serialize): Serializes the terminal's buffer to a VT sequences or HTML
|
||||||
|
- [`@xterm/addon-unicode11`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode11): Updates character widths to their unicode11 values
|
||||||
|
- [`@xterm/addon-web-links`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-web-links): Adds web link detection and interaction
|
||||||
|
- [`@xterm/addon-webgl`](https://github.com/xtermjs/xterm.js/tree/master/addons/addon-webgl): Renders xterm.js using a `canvas` element's webgl2 context
|
||||||
|
|
||||||
## Browser Support
|
## Browser Support
|
||||||
|
|
||||||
@@ -99,6 +102,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.
|
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
|
## Real-world uses
|
||||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||||
|
|
||||||
@@ -166,7 +189,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
|
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
|
||||||
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
|
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
|
||||||
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
|
- [**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.
|
- [**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.
|
- [**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.
|
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
|
||||||
@@ -193,34 +216,20 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
|||||||
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
|
- [**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
|
- [**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.
|
- [**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)
|
- [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.
|
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
|
## 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.
|
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) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
## xterm-addon-attach
|
## @xterm/addon-attach
|
||||||
|
|
||||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+.
|
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables attaching to a web socket. This addon requires xterm.js v4+.
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --save xterm-addon-attach
|
npm install --save @xterm/addon-attach
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { AttachAddon } from 'xterm-addon-attach';
|
import { AttachAddon } from '@xterm/addon-attach';
|
||||||
|
|
||||||
const terminal = new Terminal();
|
const terminal = new Terminal();
|
||||||
const attachAddon = new AttachAddon(webSocket);
|
const attachAddon = new AttachAddon(webSocket);
|
||||||
terminal.loadAddon(attachAddon);
|
terminal.loadAddon(attachAddon);
|
||||||
```
|
```
|
||||||
|
|
||||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts) for more advanced usage.
|
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-attach/typings/addon-attach.d.ts) for more advanced usage.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-search",
|
"name": "@xterm/addon-attach",
|
||||||
"version": "0.10.0",
|
"version": "0.10.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
},
|
},
|
||||||
"main": "lib/xterm-addon-search.js",
|
"main": "lib/addon-attach.js",
|
||||||
"types": "typings/xterm-addon-search.d.ts",
|
"types": "typings/addon-attach.d.ts",
|
||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-attach",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminal",
|
"terminal",
|
||||||
@@ -21,6 +21,6 @@
|
|||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"xterm": "^5.0.0"
|
"@xterm/xterm": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,13 +5,14 @@
|
|||||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, IDisposable, ITerminalAddon } from 'xterm';
|
import type { Terminal, IDisposable, ITerminalAddon } from '@xterm/xterm';
|
||||||
|
import type { AttachAddon as IAttachApi } from '@xterm/addon-attach';
|
||||||
|
|
||||||
interface IAttachOptions {
|
interface IAttachOptions {
|
||||||
bidirectional?: boolean;
|
bidirectional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AttachAddon implements ITerminalAddon {
|
export class AttachAddon implements ITerminalAddon , IAttachApi {
|
||||||
private _socket: WebSocket;
|
private _socket: WebSocket;
|
||||||
private _bidirectional: boolean;
|
private _bidirectional: boolean;
|
||||||
private _disposables: IDisposable[] = [];
|
private _disposables: IDisposable[] = [];
|
||||||
@@ -47,16 +48,14 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _sendData(data: string): void {
|
private _sendData(data: string): void {
|
||||||
// TODO: do something better than just swallowing
|
if (!this._checkOpenSocket()) {
|
||||||
// the data if the socket is not in a working condition
|
|
||||||
if (this._socket.readyState !== 1) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._socket.send(data);
|
this._socket.send(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _sendBinary(data: string): void {
|
private _sendBinary(data: string): void {
|
||||||
if (this._socket.readyState !== 1) {
|
if (!this._checkOpenSocket()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const buffer = new Uint8Array(data.length);
|
const buffer = new Uint8Array(data.length);
|
||||||
@@ -65,6 +64,22 @@ export class AttachAddon implements ITerminalAddon {
|
|||||||
}
|
}
|
||||||
this._socket.send(buffer);
|
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 {
|
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2015",
|
"target": "es2021",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"es2015"
|
"es2015"
|
||||||
@@ -13,7 +13,12 @@
|
|||||||
"strict": true,
|
"strict": true,
|
||||||
"types": [
|
"types": [
|
||||||
"../../../node_modules/@types/mocha"
|
"../../../node_modules/@types/mocha"
|
||||||
]
|
],
|
||||||
|
"paths": {
|
||||||
|
"@xterm/addon-attach": [
|
||||||
|
"../typings/addon-attach.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"./**/*",
|
"./**/*",
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import WebSocket = require('ws');
|
import WebSocket = require('ws');
|
||||||
import { openTerminal, pollFor, launchBrowser } from '../../../out-test/api/TestUtils';
|
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';
|
const APP = 'http://127.0.0.1:3001/test';
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2015",
|
"target": "es2021",
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2015"
|
"es2015"
|
||||||
],
|
],
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ITerminalAddon } from 'xterm';
|
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||||
|
|
||||||
declare module 'xterm-addon-attach' {
|
declare module '@xterm/addon-attach' {
|
||||||
export interface IAttachOptions {
|
export interface IAttachOptions {
|
||||||
/**
|
/**
|
||||||
* Whether input should be written to the backend. Defaults to `true`.
|
* Whether input should be written to the backend. Defaults to `true`.
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const addonName = 'AttachAddon';
|
const addonName = 'AttachAddon';
|
||||||
const mainFile = 'xterm-addon-attach.js';
|
const mainFile = 'addon-attach.js';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: `./out/${addonName}.js`,
|
entry: `./out/${addonName}.js`,
|
||||||
@@ -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/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/addon-canvas/typings/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,28 @@
|
|||||||
|
{
|
||||||
|
"name": "@xterm/addon-canvas",
|
||||||
|
"version": "0.6.0",
|
||||||
|
"author": {
|
||||||
|
"name": "The xterm.js authors",
|
||||||
|
"url": "https://xtermjs.org/"
|
||||||
|
},
|
||||||
|
"main": "lib/addon-canvas.js",
|
||||||
|
"types": "typings/addon-canvas.d.ts",
|
||||||
|
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-canvas",
|
||||||
|
"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",
|
||||||
|
"start-server-only": "node ../../demo/start-server-only"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@xterm/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/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._optionsService, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
|
||||||
|
this._canvas = this._coreBrowserService.mainDocument.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 as any)._core, 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, this._deviceCellWidth);
|
||||||
|
|
||||||
|
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,73 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||||
|
import type { CanvasAddon as ICanvasApi } from '@xterm/addon-canvas';
|
||||||
|
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 { CanvasRenderer } from './CanvasRenderer';
|
||||||
|
|
||||||
|
export class CanvasAddon extends Disposable implements ITerminalAddon , ICanvasApi {
|
||||||
|
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.linkifier!;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public clearTextureAtlas(): void {
|
||||||
|
this._renderer?.clearTextureAtlas();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/**
|
||||||
|
* 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, MutableDisposable, toDisposable } from 'common/Lifecycle';
|
||||||
|
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
|
import { Terminal } from '@xterm/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;
|
||||||
|
private _observerDisposable = this.register(new MutableDisposable());
|
||||||
|
|
||||||
|
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._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h));
|
||||||
|
this.register(this._coreBrowserService.onWindowChange(w => {
|
||||||
|
this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (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/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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,32 +3,33 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
|
||||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||||
import { ICoreBrowserService } from 'browser/services/Services';
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import { is256Color } from './atlas/CharAtlasUtils';
|
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||||
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
|
|
||||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
|
|
||||||
export class LinkRenderLayer extends BaseRenderLayer {
|
export class LinkRenderLayer extends BaseRenderLayer {
|
||||||
private _state: ILinkifierEvent | undefined;
|
private _state: ILinkifierEvent | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
terminal: Terminal,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
zIndex: number,
|
zIndex: number,
|
||||||
colors: IColorSet,
|
|
||||||
rendererId: number,
|
|
||||||
linkifier2: ILinkifier2,
|
linkifier2: ILinkifier2,
|
||||||
bufferService: IBufferService,
|
bufferService: IBufferService,
|
||||||
optionsService: IOptionsService,
|
optionsService: IOptionsService,
|
||||||
decorationService: IDecorationService,
|
decorationService: IDecorationService,
|
||||||
coreBrowserService: ICoreBrowserService
|
coreBrowserService: ICoreBrowserService,
|
||||||
|
themeService: IThemeService
|
||||||
) {
|
) {
|
||||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||||
|
|
||||||
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
|
||||||
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(dim: IRenderDimensions): void {
|
public resize(dim: IRenderDimensions): void {
|
||||||
@@ -53,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
|
||||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||||
this._ctx.fillStyle = this._colors.background.css;
|
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||||
} else if (e.fg && is256Color(e.fg)) {
|
} else if (e.fg && is256Color(e.fg)) {
|
||||||
// 256 color support
|
// 256 color support
|
||||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
|
||||||
} else {
|
} else {
|
||||||
this._ctx.fillStyle = this._colors.foreground.css;
|
this._ctx.fillStyle = this._themeService.colors.foreground.css;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.y1 === e.y2) {
|
if (e.y1 === e.y2) {
|
||||||
@@ -77,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
|||||||
this._state = e;
|
this._state = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
|
||||||
this._clearCurrentLink();
|
this._clearCurrentLink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,11 +3,11 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
import { IColorSet } from 'browser/Types';
|
|
||||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||||
import { ICoreBrowserService } from 'browser/services/Services';
|
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||||
|
import { Terminal } from '@xterm/xterm';
|
||||||
|
|
||||||
interface ISelectionState {
|
interface ISelectionState {
|
||||||
start?: [number, number];
|
start?: [number, number];
|
||||||
@@ -20,16 +20,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
private _state!: ISelectionState;
|
private _state!: ISelectionState;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
terminal: Terminal,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
zIndex: number,
|
zIndex: number,
|
||||||
colors: IColorSet,
|
|
||||||
rendererId: number,
|
|
||||||
bufferService: IBufferService,
|
bufferService: IBufferService,
|
||||||
coreBrowserService: ICoreBrowserService,
|
coreBrowserService: ICoreBrowserService,
|
||||||
decorationService: IDecorationService,
|
decorationService: IDecorationService,
|
||||||
optionsService: IOptionsService
|
optionsService: IOptionsService,
|
||||||
|
themeService: IThemeService
|
||||||
) {
|
) {
|
||||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||||
this._clearState();
|
this._clearState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,8 +46,9 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
super.resize(dim);
|
super.resize(dim);
|
||||||
// On resize use the base render layer's cached selection values since resize clears _state
|
// On resize use the base render layer's cached selection values since resize clears _state
|
||||||
// inside reset.
|
// inside reset.
|
||||||
if (this._selectionStart && this._selectionEnd) {
|
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
|
||||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
this._clearState();
|
||||||
|
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,18 +59,18 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public onBlur(): void {
|
public handleBlur(): void {
|
||||||
this.reset();
|
this.reset();
|
||||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onFocus(): void {
|
public handleFocus(): void {
|
||||||
this.reset();
|
this.reset();
|
||||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||||
super.onSelectionChanged(start, end, columnSelectMode);
|
super.handleSelectionChanged(start, end, columnSelectMode);
|
||||||
this._redrawSelection(start, end, columnSelectMode);
|
this._redrawSelection(start, end, columnSelectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._ctx.fillStyle = (this._coreBrowserService.isFocused
|
this._ctx.fillStyle = (this._coreBrowserService.isFocused
|
||||||
? this._colors.selectionBackgroundTransparent
|
? this._themeService.colors.selectionBackgroundTransparent
|
||||||
: this._colors.selectionInactiveBackgroundTransparent).css;
|
: this._themeService.colors.selectionInactiveBackgroundTransparent).css;
|
||||||
|
|
||||||
if (columnSelectMode) {
|
if (columnSelectMode) {
|
||||||
const startCol = start[0];
|
const startCol = start[0];
|
||||||
@@ -3,18 +3,17 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
import { CharData, ICellData } from 'common/Types';
|
|
||||||
import { GridCache } from './GridCache';
|
|
||||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
|
||||||
import { AttributeData } from 'common/buffer/AttributeData';
|
|
||||||
import { NULL_CELL_CODE, Content, UnderlineStyle } 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, ICoreBrowserService } from 'browser/services/Services';
|
|
||||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||||
import { color, css } from 'common/Color';
|
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/xterm';
|
||||||
|
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||||
|
import { GridCache } from './GridCache';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This CharData looks like a null character, which will forc a clear and render
|
* This CharData looks like a null character, which will forc a clear and render
|
||||||
@@ -31,19 +30,20 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
private _workCell = new CellData();
|
private _workCell = new CellData();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
terminal: Terminal,
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
zIndex: number,
|
zIndex: number,
|
||||||
colors: IColorSet,
|
|
||||||
alpha: boolean,
|
alpha: boolean,
|
||||||
rendererId: number,
|
|
||||||
bufferService: IBufferService,
|
bufferService: IBufferService,
|
||||||
optionsService: IOptionsService,
|
optionsService: IOptionsService,
|
||||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||||
decorationService: IDecorationService,
|
decorationService: IDecorationService,
|
||||||
coreBrowserService: ICoreBrowserService
|
coreBrowserService: ICoreBrowserService,
|
||||||
|
themeService: IThemeService
|
||||||
) {
|
) {
|
||||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||||
this._state = new GridCache<CharData>();
|
this._state = new GridCache<CharData>();
|
||||||
|
this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
|
||||||
}
|
}
|
||||||
|
|
||||||
public resize(dim: IRenderDimensions): void {
|
public resize(dim: IRenderDimensions): void {
|
||||||
@@ -51,8 +51,8 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
// Clear the character width cache if the font or width has changed
|
// Clear the character width cache if the font or width has changed
|
||||||
const terminalFont = this._getFont(false, false);
|
const terminalFont = this._getFont(false, false);
|
||||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
|
||||||
this._characterWidth = dim.scaledCharWidth;
|
this._characterWidth = dim.device.char.width;
|
||||||
this._characterFont = terminalFont;
|
this._characterFont = terminalFont;
|
||||||
this._characterOverlapCache = {};
|
this._characterOverlapCache = {};
|
||||||
}
|
}
|
||||||
@@ -93,6 +93,13 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
continue;
|
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
|
// Process any joined character ranges as needed. Because of how the
|
||||||
// ranges are produced, we know that they are valid for the characters
|
// ranges are produced, we know that they are valid for the characters
|
||||||
// and attributes of our input.
|
// and attributes of our input.
|
||||||
@@ -167,22 +174,16 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
|
|
||||||
if (cell.isInverse()) {
|
if (cell.isInverse()) {
|
||||||
if (cell.isFgDefault()) {
|
if (cell.isFgDefault()) {
|
||||||
nextFillStyle = this._colors.foreground.css;
|
nextFillStyle = this._themeService.colors.foreground.css;
|
||||||
} else if (cell.isFgRGB()) {
|
} else if (cell.isFgRGB()) {
|
||||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||||
} else {
|
} else {
|
||||||
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
|
nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
|
||||||
}
|
}
|
||||||
} else if (cell.isBgRGB()) {
|
} else if (cell.isBgRGB()) {
|
||||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||||
} else if (cell.isBgPalette()) {
|
} else if (cell.isBgPalette()) {
|
||||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
|
||||||
}
|
|
||||||
|
|
||||||
// Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is
|
|
||||||
// rarely used
|
|
||||||
if (nextFillStyle && cell.isDim()) {
|
|
||||||
nextFillStyle = color.multiplyOpacity(css.toColor(nextFillStyle), 0.5).css;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||||
@@ -232,81 +233,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
|
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(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()) {
|
|
||||||
if (!cell.isUnderlineColorDefault()) {
|
|
||||||
if (cell.isUnderlineColorRGB()) {
|
|
||||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
|
|
||||||
} else {
|
|
||||||
let fg = cell.getUnderlineColor();
|
|
||||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
|
||||||
fg += 8;
|
|
||||||
}
|
|
||||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
switch (cell.extended.underlineStyle) {
|
|
||||||
case UnderlineStyle.DOUBLE:
|
|
||||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr);
|
|
||||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr);
|
|
||||||
break;
|
|
||||||
case UnderlineStyle.CURLY:
|
|
||||||
this._curlyUnderlineAtCell(x, y, cell.getWidth());
|
|
||||||
break;
|
|
||||||
case UnderlineStyle.DOTTED:
|
|
||||||
this._dottedUnderlineAtCell(x, y, cell.getWidth());
|
|
||||||
break;
|
|
||||||
case UnderlineStyle.DASHED:
|
|
||||||
this._dashedUnderlineAtCell(x, y, cell.getWidth());
|
|
||||||
break;
|
|
||||||
case UnderlineStyle.SINGLE:
|
|
||||||
default:
|
|
||||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this._ctx.restore();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public onGridChanged(firstRow: number, lastRow: number): void {
|
public handleGridChanged(firstRow: number, lastRow: number): void {
|
||||||
// Resize has not been called yet
|
// Resize has not been called yet
|
||||||
if (this._state.cache.length === 0) {
|
if (this._state.cache.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -321,10 +251,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
this._drawForeground(firstRow, lastRow);
|
this._drawForeground(firstRow, lastRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
public onOptionsChanged(): void {
|
|
||||||
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether a character is overlapping to the next cell.
|
* Whether a character is overlapping to the next cell.
|
||||||
*/
|
*/
|
||||||
@@ -363,19 +289,4 @@ export class TextRenderLayer extends BaseRenderLayer {
|
|||||||
this._characterOverlapCache[chars] = overlaps;
|
this._characterOverlapCache[chars] = overlaps;
|
||||||
return 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,24 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { IDisposable } from 'common/Types';
|
import { IDisposable } from 'common/Types';
|
||||||
import { IColorSet } from 'browser/Types';
|
|
||||||
import { IEvent } from 'common/EventEmitter';
|
import { IEvent } from 'common/EventEmitter';
|
||||||
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
// TODO: Use core interfaces
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IRequestRedrawEvent {
|
export interface IRequestRedrawEvent {
|
||||||
start: number;
|
start: number;
|
||||||
@@ -41,16 +25,14 @@ export interface IRenderer extends IDisposable {
|
|||||||
*/
|
*/
|
||||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||||
|
|
||||||
dispose(): void;
|
handleDevicePixelRatioChange(): void;
|
||||||
setColors(colors: IColorSet): void;
|
handleResize(cols: number, rows: number): void;
|
||||||
onDevicePixelRatioChange(): void;
|
handleCharSizeChanged(): void;
|
||||||
onResize(cols: number, rows: number): void;
|
handleBlur(): void;
|
||||||
onCharSizeChanged(): void;
|
handleFocus(): void;
|
||||||
onBlur(): void;
|
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||||
onFocus(): void;
|
handleCursorMove(): void;
|
||||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
handleOptionsChanged(): void;
|
||||||
onCursorMove(): void;
|
|
||||||
onOptionsChanged(): void;
|
|
||||||
clear(): void;
|
clear(): void;
|
||||||
renderRows(start: number, end: number): void;
|
renderRows(start: number, end: number): void;
|
||||||
clearTextureAtlas?(): void;
|
clearTextureAtlas?(): void;
|
||||||
@@ -58,42 +40,34 @@ export interface IRenderer extends IDisposable {
|
|||||||
|
|
||||||
export interface IRenderLayer extends IDisposable {
|
export interface IRenderLayer extends IDisposable {
|
||||||
readonly canvas: HTMLCanvasElement;
|
readonly canvas: HTMLCanvasElement;
|
||||||
|
readonly cacheCanvas: HTMLCanvasElement;
|
||||||
|
|
||||||
|
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||||
/**
|
/**
|
||||||
* Called when the terminal loses focus.
|
* 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.
|
* Called when the cursor is moved.
|
||||||
*/
|
*/
|
||||||
onCursorMove(): void;
|
handleCursorMove(): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when options change.
|
|
||||||
*/
|
|
||||||
onOptionsChanged(): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the theme changes.
|
|
||||||
*/
|
|
||||||
setColors(colorSet: IColorSet): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the data in the grid has changed (or needs to be rendered
|
* Called when the data in the grid has changed (or needs to be rendered
|
||||||
* again).
|
* again).
|
||||||
*/
|
*/
|
||||||
onGridChanged(startRow: number, endRow: number): void;
|
handleGridChanged(startRow: number, endRow: number): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls when the selection changes.
|
* 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.
|
* Resize the render layer.
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2015",
|
"target": "es2021",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"es6"
|
"es2021"
|
||||||
],
|
],
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"outDir": "../out",
|
"outDir": "../out",
|
||||||
@@ -17,6 +17,9 @@
|
|||||||
],
|
],
|
||||||
"browser/*": [
|
"browser/*": [
|
||||||
"../../../src/browser/*"
|
"../../../src/browser/*"
|
||||||
|
],
|
||||||
|
"@xterm/addon-canvas": [
|
||||||
|
"../typings/addon-canvas.d.ts"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||||
|
* @license MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
import test from '@playwright/test';
|
||||||
|
import { ISharedRendererTestContext, injectSharedRendererTests, injectSharedRendererTestsStandalone } from '../../../out-test/playwright/SharedRendererTests';
|
||||||
|
import { ITestContext, createTestContext, openTerminal } from '../../../out-test/playwright/TestUtils';
|
||||||
|
|
||||||
|
let ctx: ITestContext;
|
||||||
|
const ctxWrapper: ISharedRendererTestContext = {
|
||||||
|
value: undefined,
|
||||||
|
skipCanvasExceptions: true
|
||||||
|
} as any;
|
||||||
|
test.beforeAll(async ({ browser }) => {
|
||||||
|
ctx = await createTestContext(browser);
|
||||||
|
await openTerminal(ctx);
|
||||||
|
ctxWrapper.value = ctx;
|
||||||
|
await ctx.page.evaluate(`
|
||||||
|
window.addon = new window.CanvasAddon(true);
|
||||||
|
window.term.loadAddon(window.addon);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
test.afterAll(async () => await ctx.page.close());
|
||||||
|
|
||||||
|
test.describe('Canvas Renderer Integration Tests', () => {
|
||||||
|
// HACK: The tests fail for an unknown reason
|
||||||
|
test.skip(({ browserName }) => browserName === 'webkit');
|
||||||
|
|
||||||
|
injectSharedRendererTests(ctxWrapper);
|
||||||
|
injectSharedRendererTestsStandalone(ctxWrapper, async () => {
|
||||||
|
await ctx.page.evaluate(`
|
||||||
|
window.addon = new window.CanvasAddon(true);
|
||||||
|
window.term.loadAddon(window.addon);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { PlaywrightTestConfig } from '@playwright/test';
|
||||||
|
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
testDir: '.',
|
||||||
|
timeout: 10000,
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'Chrome Stable',
|
||||||
|
use: {
|
||||||
|
browserName: 'chromium',
|
||||||
|
channel: 'chrome'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Firefox Stable',
|
||||||
|
use: {
|
||||||
|
browserName: 'firefox'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'WebKit',
|
||||||
|
use: {
|
||||||
|
browserName: 'webkit'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
reporter: 'list',
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run start-server-only',
|
||||||
|
port: 3000,
|
||||||
|
timeout: 120000,
|
||||||
|
reuseExistingServer: !process.env.CI
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
@@ -20,9 +20,10 @@
|
|||||||
},
|
},
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"types": [
|
"types": [
|
||||||
"../../../node_modules/@types/mocha",
|
|
||||||
"../../../node_modules/@types/node",
|
"../../../node_modules/@types/node",
|
||||||
"../../../out-test/api/TestUtils"
|
"../../../node_modules/@lunapaint/png-codec",
|
||||||
|
"../../../out-test/playwright/TestUtils",
|
||||||
|
"../../../out-test/playwright/SharedRendererTests"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
@@ -3,15 +3,25 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
import { Terminal, ITerminalAddon, IEvent } from '@xterm/xterm';
|
||||||
|
|
||||||
declare module 'xterm-addon-canvas' {
|
declare module '@xterm/addon-canvas' {
|
||||||
/**
|
/**
|
||||||
* An xterm.js addon that provides search functionality.
|
* An xterm.js addon that provides search functionality.
|
||||||
*/
|
*/
|
||||||
export class CanvasAddon implements ITerminalAddon {
|
export class CanvasAddon implements ITerminalAddon {
|
||||||
public textureAtlas?: HTMLCanvasElement;
|
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();
|
constructor();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const addonName = 'CanvasAddon';
|
const addonName = 'CanvasAddon';
|
||||||
const mainFile = 'xterm-addon-canvas.js';
|
const mainFile = 'addon-canvas.js';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: `./out/${addonName}.js`,
|
entry: `./out/${addonName}.js`,
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
## xterm-addon-fit
|
## @xterm/addon-fit
|
||||||
|
|
||||||
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables fitting the terminal's dimensions to a containing element. This addon requires xterm.js v4+.
|
An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables fitting the terminal's dimensions to a containing element. This addon requires xterm.js v4+.
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install --save xterm-addon-fit
|
npm install --save @xterm/addon-fit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { Terminal } from 'xterm';
|
import { Terminal } from '@xterm/xterm';
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
import { FitAddon } from '@xterm/addon-fit';
|
||||||
|
|
||||||
const terminal = new Terminal();
|
const terminal = new Terminal();
|
||||||
const fitAddon = new FitAddon();
|
const fitAddon = new FitAddon();
|
||||||
@@ -21,4 +21,4 @@ terminal.open(containerElement);
|
|||||||
fitAddon.fit();
|
fitAddon.fit();
|
||||||
```
|
```
|
||||||
|
|
||||||
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-fit/typings/xterm-addon-fit.d.ts) for more advanced usage.
|
See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-fit/typings/addon-fit.d.ts) for more advanced usage.
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "xterm-addon-canvas",
|
"name": "@xterm/addon-fit",
|
||||||
"version": "0.2.0",
|
"version": "0.9.0",
|
||||||
"author": {
|
"author": {
|
||||||
"name": "The xterm.js authors",
|
"name": "The xterm.js authors",
|
||||||
"url": "https://xtermjs.org/"
|
"url": "https://xtermjs.org/"
|
||||||
},
|
},
|
||||||
"main": "lib/xterm-addon-canvas.js",
|
"main": "lib/addon-fit.js",
|
||||||
"types": "typings/xterm-addon-canvas.d.ts",
|
"types": "typings/addon-fit.d.ts",
|
||||||
"repository": "https://github.com/xtermjs/xterm.js",
|
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-fit",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"terminal",
|
"terminal",
|
||||||
"canvas",
|
|
||||||
"xterm",
|
"xterm",
|
||||||
"xterm.js"
|
"xterm.js"
|
||||||
],
|
],
|
||||||
@@ -22,6 +21,6 @@
|
|||||||
"prepublishOnly": "npm run package"
|
"prepublishOnly": "npm run package"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"xterm": "^5.0.0"
|
"@xterm/xterm": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ITerminalAddon } from 'xterm';
|
import type { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||||
|
import type { FitAddon as IFitApi } from '@xterm/addon-fit';
|
||||||
|
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||||
|
|
||||||
interface ITerminalDimensions {
|
interface ITerminalDimensions {
|
||||||
/**
|
/**
|
||||||
@@ -20,11 +22,9 @@ interface ITerminalDimensions {
|
|||||||
const MINIMUM_COLS = 2;
|
const MINIMUM_COLS = 2;
|
||||||
const MINIMUM_ROWS = 1;
|
const MINIMUM_ROWS = 1;
|
||||||
|
|
||||||
export class FitAddon implements ITerminalAddon {
|
export class FitAddon implements ITerminalAddon , IFitApi {
|
||||||
private _terminal: Terminal | undefined;
|
private _terminal: Terminal | undefined;
|
||||||
|
|
||||||
constructor() {}
|
|
||||||
|
|
||||||
public activate(terminal: Terminal): void {
|
public activate(terminal: Terminal): void {
|
||||||
this._terminal = terminal;
|
this._terminal = terminal;
|
||||||
}
|
}
|
||||||
@@ -58,8 +58,9 @@ export class FitAddon implements ITerminalAddon {
|
|||||||
|
|
||||||
// TODO: Remove reliance on private API
|
// TODO: Remove reliance on private API
|
||||||
const core = (this._terminal as any)._core;
|
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;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,8 +82,8 @@ export class FitAddon implements ITerminalAddon {
|
|||||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||||
const geometry = {
|
const geometry = {
|
||||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||||
};
|
};
|
||||||
return geometry;
|
return geometry;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es2021",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"es2015"
|
||||||
|
],
|
||||||
|
"rootDir": ".",
|
||||||
|
"outDir": "../out",
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"strict": true,
|
||||||
|
"types": [
|
||||||
|
"../../../node_modules/@types/mocha"
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"browser/*": [
|
||||||
|
"../../../src/browser/*"
|
||||||
|
],
|
||||||
|
"@xterm/addon-fit": [
|
||||||
|
"../typings/addon-fit.d.ts"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./**/*",
|
||||||
|
"../../../typings/xterm.d.ts"
|
||||||
|
],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../../../src/browser"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils';
|
import { openTerminal, launchBrowser, timeout } 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';
|
const APP = 'http://127.0.0.1:3001/test';
|
||||||
|
|
||||||
@@ -41,9 +41,7 @@ describe('FitAddon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('proposeDimensions', () => {
|
describe('proposeDimensions', () => {
|
||||||
afterEach(async () => {
|
afterEach(() => unloadFit());
|
||||||
return await unloadFit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('default', async function(): Promise<any> {
|
it('default', async function(): Promise<any> {
|
||||||
await loadFit();
|
await loadFit();
|
||||||
@@ -77,14 +75,20 @@ describe('FitAddon', () => {
|
|||||||
await page.evaluate(`window.term = new Terminal()`);
|
await page.evaluate(`window.term = new Terminal()`);
|
||||||
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
|
||||||
await loadFit();
|
await loadFit();
|
||||||
assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined);
|
const dimensions: { cols: number, rows: number } | undefined = await page.evaluate(`window.fit.proposeDimensions()`);
|
||||||
|
// The value of dims will be undefined if the char measure strategy falls back to the DOM
|
||||||
|
// method, so only assert if it's not undefined.
|
||||||
|
if (dimensions) {
|
||||||
|
assert.isAbove(dimensions.cols, 85);
|
||||||
|
assert.isBelow(dimensions.cols, 88);
|
||||||
|
assert.isAbove(dimensions.rows, 24);
|
||||||
|
assert.isBelow(dimensions.rows, 29);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fit', () => {
|
describe('fit', () => {
|
||||||
afterEach(async () => {
|
afterEach(() => unloadFit());
|
||||||
return await unloadFit();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('default', async function(): Promise<any> {
|
it('default', async function(): Promise<any> {
|
||||||
await loadFit();
|
await loadFit();
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
"target": "es2015",
|
"target": "es2021",
|
||||||
"lib": [
|
"lib": [
|
||||||
"es2015"
|
"es2015"
|
||||||
],
|
],
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Terminal, ITerminalAddon } from 'xterm';
|
import { Terminal, ITerminalAddon } from '@xterm/xterm';
|
||||||
|
|
||||||
declare module 'xterm-addon-fit' {
|
declare module '@xterm/addon-fit' {
|
||||||
/**
|
/**
|
||||||
* An xterm.js addon that enables resizing the terminal to the dimensions of
|
* An xterm.js addon that enables resizing the terminal to the dimensions of
|
||||||
* its containing element.
|
* its containing element.
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const addonName = 'FitAddon';
|
const addonName = 'FitAddon';
|
||||||
const mainFile = 'xterm-addon-fit.js';
|
const mainFile = 'addon-fit.js';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
entry: `./out/${addonName}.js`,
|
entry: `./out/${addonName}.js`,
|
||||||
@@ -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/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 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.7.0",
|
||||||
|
"author": {
|
||||||
|
"name": "The xterm.js authors",
|
||||||
|
"url": "https://xtermjs.org/"
|
||||||
|
},
|
||||||
|
"main": "lib/addon-image.js",
|
||||||
|
"types": "typings/addon-image.d.ts",
|
||||||
|
"repository": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-image",
|
||||||
|
"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/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;
|
||||||
|
}
|
||||||
|
}
|
||||||