Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c3663470 | ||
|
|
3a310f1597 | ||
|
|
4d0217f816 | ||
|
|
559e932a2e | ||
|
|
aa427010e1 | ||
|
|
1aec57e8c0 | ||
|
|
33b65bbfaa | ||
|
|
8e39650b5f | ||
|
|
f148238117 | ||
|
|
6057586ff3 | ||
|
|
6a2c4b9823 | ||
|
|
e2cfd94005 | ||
|
|
9fe8ea8de2 | ||
|
|
46311b87a2 | ||
|
|
692ca1c9a3 | ||
|
|
bf5c1f0f78 | ||
|
|
467a93c8d9 | ||
|
|
3841edadb5 | ||
|
|
dc332480cc | ||
|
|
a194263183 | ||
|
|
dd7081f948 | ||
|
|
999fce4e52 | ||
|
|
0d49d1f64d | ||
|
|
11df1e1d80 | ||
|
|
a74ca25b09 | ||
|
|
32231dcd41 | ||
|
|
04a847f0e2 | ||
|
|
276e65fa0b | ||
|
|
a1421b151e | ||
|
|
71f5965f8e | ||
|
|
59fc4af9a0 | ||
|
|
381e6c1740 | ||
|
|
9a54cf639b | ||
|
|
01a4ec21da | ||
|
|
a65bd1a77c | ||
|
|
6439f6e856 | ||
|
|
d24caf14a4 | ||
|
|
52f132a570 | ||
|
|
424d3257d1 | ||
|
|
f47f095b89 | ||
|
|
de26be56c1 | ||
|
|
f964a34b34 | ||
|
|
6576d513b0 | ||
|
|
4af2d1ed41 | ||
|
|
9e27c231e5 | ||
|
|
ae3684c897 | ||
|
|
d4c621a182 | ||
|
|
9c68eb3b11 | ||
|
|
77cb697990 | ||
|
|
cee432a66a | ||
|
|
a553415216 | ||
|
|
cd8477a942 | ||
|
|
811b3cc326 | ||
|
|
a99cac45f7 | ||
|
|
3d34881c4e | ||
|
|
0340d91150 | ||
|
|
5c3315251b | ||
|
|
e769e787a8 | ||
|
|
96ed32698b | ||
|
|
e66928da20 | ||
|
|
c762885c4b | ||
|
|
63ced6f302 | ||
|
|
82f6547f10 | ||
|
|
841a0e010d | ||
|
|
85f25e77f5 | ||
|
|
2adaed87d1 | ||
|
|
4c93d8ddaa | ||
|
|
b35fa9576e | ||
|
|
091c42f7e6 | ||
|
|
6601133912 | ||
|
|
9273e8175e | ||
|
|
cd75d6fb68 | ||
|
|
01bfa1df4b | ||
|
|
123e074c16 | ||
|
|
929150a8e1 | ||
|
|
e3eb1b81b3 | ||
|
|
04c4448f97 | ||
|
|
0e45909c7e | ||
|
|
44366ffd3c | ||
|
|
49cf1a2cea | ||
|
|
86800d2d71 | ||
|
|
41fdc9071a | ||
|
|
14d75364eb | ||
|
|
2127128ef5 | ||
|
|
95ebba6fdb | ||
|
|
ff63d3eb23 | ||
|
|
05b8ef1727 | ||
|
|
458f2d8bf1 | ||
|
|
87a84b608c | ||
|
|
dd7a9c3b7a | ||
|
|
d1ca0d7839 | ||
|
|
3419480b98 | ||
|
|
a003a0c585 | ||
|
|
e3dfe4bc32 | ||
|
|
c9ec7aa215 | ||
|
|
7675be7da3 | ||
|
|
1a03b703f1 | ||
|
|
9165581266 | ||
|
|
6a770296f9 | ||
|
|
cbfb99a5cf | ||
|
|
bae3f64c10 | ||
|
|
7c7102d8a6 | ||
|
|
8e9ba23227 | ||
|
|
1d91e3c5bf | ||
|
|
45d4622920 | ||
|
|
b7da4b6bfe | ||
|
|
fdf51c119b | ||
|
|
6de8c91059 | ||
|
|
490f0ad329 | ||
|
|
882aea357f | ||
|
|
d8eedfa1c7 | ||
|
|
90e57c9b3c | ||
|
|
a269e7f5d9 | ||
|
|
4a3c1a4cc6 | ||
|
|
804cb36adc | ||
|
|
5aaf9ef11b | ||
|
|
c65dd1f476 | ||
|
|
05f136b3da | ||
|
|
1c4cb8c620 | ||
|
|
fec66c7dac | ||
|
|
e82f78fc84 | ||
|
|
67d6ad57c3 | ||
|
|
48f1c4667a | ||
|
|
6a9da97163 | ||
|
|
7b68cb9fe7 | ||
|
|
6691f80906 | ||
|
|
bdfe877c8f | ||
|
|
ee3668eb03 | ||
|
|
4a443c4a45 | ||
|
|
d964c23c43 | ||
|
|
20d4c64ffe | ||
|
|
bf3709f4c5 | ||
|
|
8f22d8dda4 | ||
|
|
0f6a8d3991 | ||
|
|
5b8da8c89a | ||
|
|
f2e5ea5783 | ||
|
|
cb888c343e | ||
|
|
18f37440dc | ||
|
|
3582f8733d | ||
|
|
b84dba2e8d | ||
|
|
61fc4ca8a6 | ||
|
|
bbd234b951 | ||
|
|
0545cfa121 | ||
|
|
26969b725e | ||
|
|
eb31732af0 | ||
|
|
9c08f78017 | ||
|
|
54dcbeb6cf | ||
|
|
a52aab4618 | ||
|
|
5f5a49995a | ||
|
|
ac7239732d | ||
|
|
68d537fb28 | ||
|
|
7c3bf3a130 | ||
|
|
7ce071c575 | ||
|
|
42f1aba00c | ||
|
|
dfb1b4db95 | ||
|
|
6900bbc07a | ||
|
|
d4b507ddb1 | ||
|
|
1bad71f13a | ||
|
|
63b3a6f323 | ||
|
|
ba7976f3b1 | ||
|
|
b5b350fdcf | ||
|
|
87e292414d | ||
|
|
5620da49d8 | ||
|
|
3c8f336689 | ||
|
|
f1c9d37c83 | ||
|
|
08603c7975 | ||
|
|
318dc6fcf6 | ||
|
|
68adadf3f4 | ||
|
|
5e3ebbde5e | ||
|
|
6d9d2a3304 | ||
|
|
73bc1c577a | ||
|
|
aec4c908d3 | ||
|
|
9aad2f7ca5 | ||
|
|
af9d17760d | ||
|
|
4557f857f7 | ||
|
|
ed58c42513 | ||
|
|
7e393dfa8c | ||
|
|
0a70b182a7 | ||
|
|
932a8d5028 | ||
|
|
054592520e | ||
|
|
38debf77f8 | ||
|
|
117c79e4e7 | ||
|
|
3a4ad37177 | ||
|
|
ace612e575 | ||
|
|
e628a9be76 | ||
|
|
835d5a16ea | ||
|
|
169cbb3fca | ||
|
|
2669576b6c | ||
|
|
3202ddc96d | ||
|
|
35ee3610f7 | ||
|
|
45f5a710d6 | ||
|
|
1395b0f03c | ||
|
|
38efb1a02d | ||
|
|
22308fb37c | ||
|
|
100143bd02 | ||
|
|
8c20a2e26b | ||
|
|
e24dca3e29 | ||
|
|
218a4014c7 | ||
|
|
0ad7b8e27b | ||
|
|
f1504e5c0f | ||
|
|
f0d872f7fe | ||
|
|
ff18b193eb | ||
|
|
8ae8705ae6 | ||
|
|
d585515de1 | ||
|
|
8dda3e68f8 | ||
|
|
4c7e5d2201 | ||
|
|
cc41fefe31 | ||
|
|
9a3751d7fb | ||
|
|
64b0a9482c | ||
|
|
221a4353a4 | ||
|
|
989c24cbc3 | ||
|
|
654f4bd5ae | ||
|
|
43ffa957ab | ||
|
|
a9a506f7db | ||
|
|
6e71fb0fb8 | ||
|
|
4d52cd3c66 | ||
|
|
27b1c89f43 | ||
|
|
867ce262e8 | ||
|
|
45e4486bf5 | ||
|
|
545d0782c9 | ||
|
|
ede1d5f7c8 | ||
|
|
5adf962eb5 | ||
|
|
9293616f02 | ||
|
|
7e779cd195 | ||
|
|
dfd17be8bb | ||
|
|
a44f496ec7 | ||
|
|
c0f4a8eb7c | ||
|
|
086b2c04e0 | ||
|
|
4809b04f26 | ||
|
|
4fecf05dab | ||
|
|
285b0b6702 | ||
|
|
a1b8f96d50 | ||
|
|
c345dba591 | ||
|
|
8dcb4698d3 | ||
|
|
f9bc3c511b | ||
|
|
0d0f4b048c | ||
|
|
91cf9e1ba8 | ||
|
|
5d05c3a5f3 | ||
|
|
628e648b57 | ||
|
|
72bb1b030a | ||
|
|
6c6d90a698 | ||
|
|
356959e338 | ||
|
|
6c3b6b4655 | ||
|
|
ee935cc68c | ||
|
|
57959249d8 | ||
|
|
bb4db71958 | ||
|
|
8c3376911f | ||
|
|
1487b8d02d | ||
|
|
3705d8159d | ||
|
|
6a3ed43ad2 | ||
|
|
12e7f80037 | ||
|
|
410d506f34 | ||
|
|
d29a527724 | ||
|
|
20307ef637 | ||
|
|
95217ca931 | ||
|
|
b4ccbe042b | ||
|
|
e167686b6d | ||
|
|
b6481f120b | ||
|
|
fb63b3475b | ||
|
|
2f1c1cd172 | ||
|
|
03e31571a9 | ||
|
|
c6370ba0cf | ||
|
|
432c2cc369 | ||
|
|
3637b83bbb | ||
|
|
ddc9466916 | ||
|
|
df9d20bfdc | ||
|
|
e9af4d0e80 | ||
|
|
f38a34c6da | ||
|
|
41b5fd8ddf | ||
|
|
22ef5661eb | ||
|
|
4e2c30c81d | ||
|
|
8f91e3bcde | ||
|
|
5aff7c2c1e | ||
|
|
b4bb433c1b | ||
|
|
c405b58456 | ||
|
|
445689c679 | ||
|
|
1c0d7771d7 | ||
|
|
a8964a15dd | ||
|
|
bdfdcf3508 | ||
|
|
0276ab4f85 | ||
|
|
f83394b0ab | ||
|
|
13d5ad6506 | ||
|
|
7751933c8c | ||
|
|
42d7d3aefc | ||
|
|
0f488e59d7 | ||
|
|
1066816142 | ||
|
|
6e3c16a2c2 | ||
|
|
3d56b152f3 | ||
|
|
24fad1126c | ||
|
|
53463681de | ||
|
|
2aff2eb710 | ||
|
|
c748b02111 | ||
|
|
73e35b9293 | ||
|
|
1cccec6398 | ||
|
|
1ca6e51564 | ||
|
|
52be55852e | ||
|
|
824a03e0af | ||
|
|
3c02c9ce66 | ||
|
|
adb0cf41ab | ||
|
|
de683b35b2 | ||
|
|
767e3570ac | ||
|
|
75a45808c3 | ||
|
|
7ca6873a5e | ||
|
|
908b06cfa7 | ||
|
|
2d786910c8 | ||
|
|
5b04451b05 | ||
|
|
4d5e305a25 | ||
|
|
4dff828374 | ||
|
|
fea8ba03b8 | ||
|
|
918ee16f86 | ||
|
|
b79ab7212c | ||
|
|
956b773421 | ||
|
|
cd3418517e | ||
|
|
285911cd98 | ||
|
|
5acc45f08b | ||
|
|
eabb25c796 | ||
|
|
c192a27d93 | ||
|
|
d5af79fc5f | ||
|
|
90b9a487cd | ||
|
|
4617bab3a0 | ||
|
|
8989a09853 | ||
|
|
4a4a17966a | ||
|
|
7db0b181bc | ||
|
|
3ce71b7f8f | ||
|
|
c2568a6857 | ||
|
|
834b266b7f | ||
|
|
5d9b761f33 | ||
|
|
35fc10b0e6 | ||
|
|
ffd7d5513b | ||
|
|
656567aed0 | ||
|
|
82fdcc0771 | ||
|
|
c2b5a243f2 | ||
|
|
6e9cd35176 | ||
|
|
9890a8b20f | ||
|
|
20975278c5 | ||
|
|
b43ee08dd2 | ||
|
|
3ae96a2266 | ||
|
|
87fee8d024 | ||
|
|
09e42af730 | ||
|
|
44fde26f04 | ||
|
|
ac9b71549a | ||
|
|
328086f206 | ||
|
|
b3e8197111 | ||
|
|
099c7d8f04 | ||
|
|
507c4dc5d2 | ||
|
|
6bc6ea56a5 | ||
|
|
e97882e5ad | ||
|
|
85dada2eed | ||
|
|
8386ec1c19 | ||
|
|
ceb1432f75 | ||
|
|
8d722d20e2 | ||
|
|
054abd15ea | ||
|
|
724aa9d6c7 | ||
|
|
ab5fc98627 | ||
|
|
0dfc9b2118 | ||
|
|
8bbc1fa287 | ||
|
|
c1f8225c4f | ||
|
|
0ad1080133 | ||
|
|
1736c1e12d | ||
|
|
4717619f76 | ||
|
|
7f761b5b9e | ||
|
|
c00a3726f3 | ||
|
|
5ef399df6f | ||
|
|
f8e9d02566 | ||
|
|
d8afdb1b6b | ||
|
|
2b4c5adb3c | ||
|
|
7334e6ab19 | ||
|
|
137b499538 | ||
|
|
6024e7c5fe | ||
|
|
aa1e1c5d94 | ||
|
|
f055e0ef03 | ||
|
|
2f618b37c1 | ||
|
|
3acd1eaecc | ||
|
|
435cf91ec7 | ||
|
|
6e451f58b8 | ||
|
|
7a9f291df9 | ||
|
|
9dfce63e55 | ||
|
|
139a246436 | ||
|
|
343cc2b7b2 | ||
|
|
0375e0064b | ||
|
|
57e35f836e | ||
|
|
685757b04b | ||
|
|
210a312fbf | ||
|
|
c366ee20b8 | ||
|
|
b9baacac3a | ||
|
|
45fea0e790 | ||
|
|
cded6f80f6 | ||
|
|
9c1bee300d | ||
|
|
ef4cda2cc6 | ||
|
|
d315dd6684 | ||
|
|
8a5891ede2 | ||
|
|
bce81f304e | ||
|
|
3971fc9b4e | ||
|
|
4edbd507e5 | ||
|
|
b162582e66 | ||
|
|
be32695e16 | ||
|
|
9893338020 | ||
|
|
cfdec9ff90 | ||
|
|
02893dcbb8 | ||
|
|
ce6139e604 | ||
|
|
a23354f018 | ||
|
|
41fd563144 | ||
|
|
a37449a8cd | ||
|
|
551ccc0f2f | ||
|
|
108aa5ab31 | ||
|
|
362d7698f7 | ||
|
|
edb06c6b2a | ||
|
|
6cefb4c634 | ||
|
|
cd3eea5b48 | ||
|
|
5b5eb48867 | ||
|
|
09624c18e0 | ||
|
|
9bcecd4e2e | ||
|
|
52e71a2530 | ||
|
|
6cadd7c4a8 | ||
|
|
b14308f2c4 | ||
|
|
2c3949375e | ||
|
|
e3e4dcb5d1 | ||
|
|
e9f4d39f2b | ||
|
|
d777513b87 | ||
|
|
1d2f356cfd | ||
|
|
418f2c6cdf | ||
|
|
3cab8bede1 | ||
|
|
58ce5e2110 | ||
|
|
20ff107768 | ||
|
|
16393b228d | ||
|
|
0f8d22547e | ||
|
|
c069e94a4c | ||
|
|
a1a4cac113 | ||
|
|
25ff77af03 | ||
|
|
536b3a8def | ||
|
|
d090cbe4b9 | ||
|
|
9a32852e61 | ||
|
|
e48b15e859 | ||
|
|
89cf45c514 | ||
|
|
a732952677 | ||
|
|
0c200f2feb | ||
|
|
728eeebcc7 | ||
|
|
b60ab119b2 | ||
|
|
c7fdff2f40 | ||
|
|
2eeb7b8ea5 | ||
|
|
0535f5d1b3 | ||
|
|
aeae688b4c | ||
|
|
af392a6305 | ||
|
|
ba6fcddc43 | ||
|
|
dbc6b12ec2 | ||
|
|
645cd2eb2c | ||
|
|
b158176862 | ||
|
|
a1035269d5 | ||
|
|
f5375c3a99 | ||
|
|
a55a7f3695 | ||
|
|
826cca7088 | ||
|
|
0f3d82a2a5 | ||
|
|
dc14f5226d | ||
|
|
27a3347c3f | ||
|
|
2d77fcf2fb | ||
|
|
9d2bbd199c | ||
|
|
b72cdadd11 | ||
|
|
381d3e1444 | ||
|
|
b97eaf2c52 | ||
|
|
513847fd80 | ||
|
|
7b2500bfd6 | ||
|
|
882d5a86d3 | ||
|
|
235982362c | ||
|
|
aaaec75d73 | ||
|
|
30b88113a8 | ||
|
|
994895af6d | ||
|
|
a61ba2e140 | ||
|
|
a5a03c40f2 | ||
|
|
83fe87d012 | ||
|
|
3713d282d8 | ||
|
|
8aa65d06f5 | ||
|
|
fd97fead60 | ||
|
|
9eb107a6e9 | ||
|
|
c0ab2b9a7c | ||
|
|
a058395014 | ||
|
|
453da35f51 | ||
|
|
d254d69ee2 | ||
|
|
ff19ba8e33 | ||
|
|
fcde7be62a | ||
|
|
45b4429712 | ||
|
|
9942dc384d | ||
|
|
55993ada9b | ||
|
|
c8910cc5fe | ||
|
|
f9767073ba | ||
|
|
56f9d423fe | ||
|
|
716a8d5926 | ||
|
|
9593cb0cc8 | ||
|
|
7ed7505ce4 | ||
|
|
6ff4ae7194 | ||
|
|
9e863d0377 | ||
|
|
13d4ab6f8d | ||
|
|
0d7ecd9129 | ||
|
|
dd096c2e8b | ||
|
|
0decd77253 | ||
|
|
2bfe74490a | ||
|
|
20760d09e4 | ||
|
|
73f40ef7d2 | ||
|
|
9b085d2bdd | ||
|
|
fb7e5d590c | ||
|
|
0fe59df2e9 | ||
|
|
a3ce881897 | ||
|
|
58c50f08db | ||
|
|
984e027c87 | ||
|
|
b853aa4929 | ||
|
|
4fd630ec5c | ||
|
|
ec96c40fde | ||
|
|
cf8fa10205 | ||
|
|
d8d9d904b5 | ||
|
|
4a13813941 | ||
|
|
cd7911a3d4 | ||
|
|
3625a2278d | ||
|
|
7d108999ff | ||
|
|
e3dc36c123 | ||
|
|
11541be4ae | ||
|
|
ceffb35445 | ||
|
|
e1957ae6aa | ||
|
|
5a05db727c | ||
|
|
1f692ed047 | ||
|
|
c3eea02855 | ||
|
|
c6d66d7b0d | ||
|
|
9c0ce1d742 | ||
|
|
ac7db87fd0 | ||
|
|
8b3b7535c6 | ||
|
|
c8b500c182 | ||
|
|
24feaabd9a | ||
|
|
8c980bc30d | ||
|
|
6f21385cd8 | ||
|
|
83f9d59d91 | ||
|
|
3010760352 | ||
|
|
82c16dee73 | ||
|
|
7e8dd9647f | ||
|
|
b594641b26 | ||
|
|
5df9808d4c | ||
|
|
2cee66909b | ||
|
|
da8151b1b5 | ||
|
|
a3cb34fbf1 | ||
|
|
9a1162d7f2 | ||
|
|
591b09a62b | ||
|
|
f6f3e78e2c | ||
|
|
048b10ebf6 | ||
|
|
c882dcc353 | ||
|
|
82897ed56c | ||
|
|
cd06786d16 | ||
|
|
74d91a6bdd | ||
|
|
4a1ebbfb56 | ||
|
|
86c9b3f92c | ||
|
|
4fbd1adaa9 | ||
|
|
004ea61b83 | ||
|
|
9be57f72f0 | ||
|
|
e5ee76ded4 | ||
|
|
87f21f14a1 | ||
|
|
ef16811268 | ||
|
|
989f5873f5 | ||
|
|
307744be66 | ||
|
|
768539a57c | ||
|
|
22c4197a1d | ||
|
|
d4b75e4909 | ||
|
|
4b5de031ea | ||
|
|
b66b0aae18 | ||
|
|
36a1c5b922 | ||
|
|
63f711e634 | ||
|
|
516903e29e | ||
|
|
5cf9423699 | ||
|
|
dbb2320e83 | ||
|
|
0cb6138cd0 | ||
|
|
bf21097430 | ||
|
|
55d6daf8ee | ||
|
|
6e1e63e666 | ||
|
|
b27d5bb08d | ||
|
|
e63831febc | ||
|
|
4ab79c908a | ||
|
|
b85083c3e8 | ||
|
|
97f592ce8b | ||
|
|
70da707569 | ||
|
|
63f24be2b9 | ||
|
|
0d6be9e03e | ||
|
|
3cbb21a966 | ||
|
|
1fd7348938 | ||
|
|
ae32893732 | ||
|
|
4f82cf26f0 | ||
|
|
b67b65cd1e | ||
|
|
0eeec13653 | ||
|
|
5d1f744282 | ||
|
|
387d5b056d | ||
|
|
cbf7419601 | ||
|
|
5c1a4a7d01 | ||
|
|
cbc6d7ce32 | ||
|
|
b80832ac4c | ||
|
|
3716aad202 | ||
|
|
efbd542964 | ||
|
|
888070830d | ||
|
|
9905e2aa1d | ||
|
|
a59af6606b | ||
|
|
d1213e5656 | ||
|
|
0e14798d69 | ||
|
|
c8ab687fa0 | ||
|
|
e5f5c2baf2 | ||
|
|
2f861602c8 | ||
|
|
4f0cc01485 | ||
|
|
dd41ab38bb | ||
|
|
255cb5de3a | ||
|
|
b44a11c42e | ||
|
|
90b2f0e6a1 | ||
|
|
811aa9c413 | ||
|
|
842c7132ec | ||
|
|
efb67edc36 | ||
|
|
ed7741e006 | ||
|
|
cfe936a0b4 | ||
|
|
4ddf91dbbe | ||
|
|
59be054ca1 | ||
|
|
b794dc017d | ||
|
|
4443ed69c7 | ||
|
|
ab9b84898e | ||
|
|
a41e183b88 | ||
|
|
949239dbdc | ||
|
|
421ccc5695 | ||
|
|
5d68ed6e5e | ||
|
|
021f8c1ba0 | ||
|
|
f456b14b3f | ||
|
|
7a1ef19146 | ||
|
|
b9cbdf6a0d | ||
|
|
7639131c12 | ||
|
|
289b4eeb23 | ||
|
|
404a285eaf | ||
|
|
b20fe80f9b | ||
|
|
ff9814a40d | ||
|
|
855964fbe1 | ||
|
|
606c68dd0c | ||
|
|
735dfb4710 | ||
|
|
ab69bfbb32 | ||
|
|
70771430b5 | ||
|
|
6741e92cc6 | ||
|
|
31ce5c1826 | ||
|
|
1f113b1d29 | ||
|
|
41f6ee4f6d | ||
|
|
6e546c0f30 | ||
|
|
7cae8e51ca | ||
|
|
bf4dd7de60 | ||
|
|
ff6ef1b63a | ||
|
|
34034b849f | ||
|
|
f2d8709dd8 | ||
|
|
e137530581 | ||
|
|
be7788dda6 | ||
|
|
7c471f5389 | ||
|
|
9e62c8692f | ||
|
|
18fb134910 | ||
|
|
454fd4bfb1 | ||
|
|
131e6ff949 | ||
|
|
c51fa238ff | ||
|
|
2e3f9e7094 | ||
|
|
76a3dbf883 | ||
|
|
07cf029497 | ||
|
|
c8017345f1 | ||
|
|
0d2cc7489e | ||
|
|
f3a37a9522 | ||
|
|
6a71717751 | ||
|
|
dc822787d0 | ||
|
|
5011e629b1 | ||
|
|
bc35f28f51 | ||
|
|
97feb64e94 | ||
|
|
94bbc4e09d | ||
|
|
ad7d2262ec | ||
|
|
1114977a21 | ||
|
|
a8c5b159a9 | ||
|
|
a91e0e234a | ||
|
|
af5b3f704e | ||
|
|
45c9763092 | ||
|
|
db8ba17147 | ||
|
|
1972bf4bb3 | ||
|
|
62cf0936dc | ||
|
|
a9d6686e14 | ||
|
|
99aa142cef | ||
|
|
f8f1355155 | ||
|
|
ae28814041 | ||
|
|
183fd2fe79 | ||
|
|
8324462b32 | ||
|
|
93e5996422 | ||
|
|
f089b997c0 | ||
|
|
51d2064061 | ||
|
|
712b91a971 | ||
|
|
cbe6c64a92 | ||
|
|
64ed8309a1 | ||
|
|
b5ba9ae771 | ||
|
|
18f6556061 | ||
|
|
6dc49b7d01 | ||
|
|
74a6dfb197 | ||
|
|
73e0db1912 | ||
|
|
9ade1f0a5d | ||
|
|
f8fbdf31fa | ||
|
|
a0f390ef54 | ||
|
|
ec156f62e1 | ||
|
|
a3958a1cba | ||
|
|
7c8b64aaf1 | ||
|
|
b13826441c | ||
|
|
0bdf1e8265 | ||
|
|
5506af5322 | ||
|
|
6439aca1d8 | ||
|
|
65850c1c20 | ||
|
|
963db4d2fd | ||
|
|
5c061195c0 | ||
|
|
0104e4214e | ||
|
|
baaf61b3a3 | ||
|
|
b9976c3d17 | ||
|
|
1cceda6052 | ||
|
|
3e734fab79 | ||
|
|
c41b35ae9c | ||
|
|
e478918ea7 | ||
|
|
e1c99b28f0 | ||
|
|
45c6008b27 | ||
|
|
fba4f5f261 | ||
|
|
bce0cce74d | ||
|
|
c2d78e57d0 | ||
|
|
bb8e06592c | ||
|
|
421a885acd | ||
|
|
d62e4e25b7 | ||
|
|
ec9f2702eb | ||
|
|
b97cedcf5b | ||
|
|
39fcbe61c0 | ||
|
|
0f04f9fbb4 | ||
|
|
abfa58fca9 | ||
|
|
78d734117b | ||
|
|
f48da12c16 | ||
|
|
3fa4b4941f | ||
|
|
222c42f7ea | ||
|
|
34eb08952c | ||
|
|
e9b55930d8 | ||
|
|
1e9fb8696a | ||
|
|
81b4d8fe9c | ||
|
|
23bd60a733 | ||
|
|
85c02f10bf | ||
|
|
70489e5035 | ||
|
|
1e41b11319 | ||
|
|
2a187a26e0 | ||
|
|
2311d7d66b | ||
|
|
bbc9a010b9 | ||
|
|
b75f52734d | ||
|
|
a19b05f813 | ||
|
|
c46304c716 | ||
|
|
e20d2caa0e | ||
|
|
a846e8c08d | ||
|
|
c332aeb51f | ||
|
|
bb830eb1ae | ||
|
|
6d1064fc01 | ||
|
|
5ca15cfe3b | ||
|
|
17964a517c | ||
|
|
8cb7a9dabe | ||
|
|
5dc57a1305 | ||
|
|
0d65cb3f69 | ||
|
|
ae957ecb3e | ||
|
|
d03f5b0c2a | ||
|
|
e5d57b08c8 | ||
|
|
399779692d |
@@ -1,11 +0,0 @@
|
||||
<!-- Enter your issue description here -->
|
||||
|
||||
## Details
|
||||
- Browser and browser version:
|
||||
- OS version:
|
||||
- xterm.js version:
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
---
|
||||
|
||||
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
|
||||
<!-- Describe the bug here. -->
|
||||
|
||||
## Details
|
||||
- Browser and browser version:
|
||||
- OS version:
|
||||
- xterm.js version:
|
||||
|
||||
### Steps to reproduce
|
||||
|
||||
1.
|
||||
2.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature or enhancement
|
||||
---
|
||||
|
||||
<!-- ⚠️ Please search existing issues to avoid creating duplicates. ⚠️ -->
|
||||
<!-- Describe the enhancement here. -->
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: Question
|
||||
about: The issue tracker is not for questions. Please ask questions on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
|
||||
---
|
||||
|
||||
🛑 The issue tracker is not for questions 🛑
|
||||
|
||||
If you have a question, please ask it on https://stackoverflow.com/questions/tagged/xtermjs or https://gitter.im/sourcelair/xterm.js.
|
||||
+3
-1
@@ -15,9 +15,11 @@ build/
|
||||
.vscode/
|
||||
.DS_Store
|
||||
fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
# Directories needed for code coverage
|
||||
/coverage/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
|
||||
+44
-12
@@ -1,16 +1,48 @@
|
||||
node_modules/
|
||||
*.swp
|
||||
.lock-wscript
|
||||
lib/*.test.js
|
||||
lib/*.test.js.map
|
||||
# Blacklist - exclude everything except npm defaults such as LICENSE, etc
|
||||
*
|
||||
!*/
|
||||
|
||||
# Whitelist - entries to be included must be negated with "!"
|
||||
!*.js
|
||||
!*.json
|
||||
|
||||
# Whitelist - dist/
|
||||
!dist/**/*.js
|
||||
!dist/**/*.js.map
|
||||
|
||||
!dist/**/*.css
|
||||
|
||||
# Whitelist - lib/
|
||||
!lib/**/*.d.ts
|
||||
|
||||
!lib/**/*.js
|
||||
!lib/**/*.js.map
|
||||
|
||||
!lib/**/*.css
|
||||
|
||||
# Whitelist - src/
|
||||
!src/**/*.ts
|
||||
!src/**/*.d.ts
|
||||
|
||||
!src/**/*.js
|
||||
!src/**/*.js.map
|
||||
|
||||
!src/**/*.css
|
||||
|
||||
# Whitelist - typings/
|
||||
!typings/*.d.ts
|
||||
|
||||
# Blacklist - (normal behavior) these will override any whitelist
|
||||
*.test.ts
|
||||
*.test.d.ts
|
||||
*.test.js
|
||||
*.test.js.map
|
||||
lib/test/
|
||||
Makefile.gyp
|
||||
*.Makefile
|
||||
*.target.gyp.mk
|
||||
*.node
|
||||
example/*.log
|
||||
|
||||
docs/
|
||||
npm-debug.log
|
||||
/.idea/
|
||||
.env
|
||||
.vscode/
|
||||
build/
|
||||
fixtures/
|
||||
coverage/
|
||||
demo/
|
||||
|
||||
-20
@@ -1,20 +0,0 @@
|
||||
language: node_js
|
||||
os:
|
||||
- linux
|
||||
node_js:
|
||||
- 6
|
||||
before_install:
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq install g++-4.8 ; fi
|
||||
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export CXX=g++-4.8 ; fi
|
||||
- npm install -g npm@5.1.0
|
||||
env:
|
||||
matrix:
|
||||
- NPM_COMMAND=tsc
|
||||
- NPM_COMMAND=lint
|
||||
- NPM_COMMAND=test
|
||||
notifications:
|
||||
email: false
|
||||
script: npm run $NPM_COMMAND
|
||||
after_success: npm run coveralls
|
||||
Vendored
-23
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "Mocha Tests",
|
||||
"cwd": "${workspaceRoot}",
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha",
|
||||
"windows": {
|
||||
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd"
|
||||
},
|
||||
"runtimeArgs": [
|
||||
"--colors",
|
||||
"--recursive",
|
||||
"${workspaceRoot}/lib"
|
||||
],
|
||||
"sourceMaps": true,
|
||||
"outFiles": [ "${workspaceRoot}/lib/**/*.js" ],
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/dist": true,
|
||||
"**/build": true,
|
||||
"**/lib": true
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
List of xterm.js contributors. Updated before every release.
|
||||
|
||||
7PH <b.raymond@cern.ch>
|
||||
7PH <b.raymond@protonmail.com>
|
||||
Adrian Labbé <adrilabbelol@gmail.com>
|
||||
Aleksandr Andrienko <aandrienko@codenvy.com>
|
||||
Aleksandr Andriienko <oandriie@redhat.com>
|
||||
Alessandro Nadalin <alessandro.nadalin@gmail.com>
|
||||
Alexander Olsson <noseglid@gmail.com>
|
||||
Alexandre Petit-Pas <petitpasalexandre@gmail.com>
|
||||
Alexey Kontsevoy <alexey@gravitational.com>
|
||||
Andres Mejia <amejia004@gmail.com>
|
||||
Anish Athalye <me@anishathalye.com>
|
||||
@@ -18,9 +21,12 @@ Austin Robertson <austinrobertson@gmail.com>
|
||||
ayapi <colors.aya@gmail.com>
|
||||
Ben Hall <ben@benhall.me.uk>
|
||||
Benjamin Fischer <benjamin.fischer@rwth-aachen.de>
|
||||
Benjamin Raymond <b.raymond@cern.ch>
|
||||
Benjamin Woodruff <github@benjam.info>
|
||||
Bill Church <billchurch@users.noreply.github.com>
|
||||
Bob Reid <bobreid@Bobs-MacBook-Pro.local>
|
||||
bottleofwater <nison.mael+bottleofwater@gmail.com>
|
||||
Brandon Bayer <b@bayer.ws>
|
||||
Brian Mock <brian@mockbrian.com>
|
||||
Bruno Ribeiro <b.m.fernandes.ribeiro@gmail.com>
|
||||
Bruno Ribeito <b.m.fernandes.ribeiro@gmail.com>
|
||||
@@ -32,6 +38,7 @@ Christopher Jeffrey <chjjeffrey@gmail.com>
|
||||
coderaiser <mnemonic.enemy@gmail.com>
|
||||
Damien Tournoud <damien@platform.sh>
|
||||
Dan Brown <jdanbrown@gmail.com>
|
||||
Daniel Griffen <dagriffe@microsoft.com>
|
||||
Daniel Griffen <daniel@griffen.io>
|
||||
Daniel Imms <daimms@microsoft.com>
|
||||
Daniel Risacher <drisacher@gmail.com>
|
||||
@@ -43,6 +50,7 @@ Edgar Andrés Margffoy Tuay <andfoy@gmail.com>
|
||||
Elliot Saba <staticfloat@gmail.com>
|
||||
Exile <exile@chamber.ee>
|
||||
Felipe Gasper <felipe@felipegasper.com>
|
||||
Felix <30559812+felixse@users.noreply.github.com>
|
||||
ficristo <ficristo.work@gmail.com>
|
||||
Gary Ritchie <gary@rstudio.com>
|
||||
hiro-su <h.sugipon@gmail.com>
|
||||
@@ -53,46 +61,65 @@ irokas <akasidiari@inf.uth.gr>
|
||||
Jakob Gillich <jakob@gillich.me>
|
||||
Jan Kuri <jkuri88@gmail.com>
|
||||
Jean Bruenn <himself@jeanbruenn.info>
|
||||
Jeff Principe <princjef@gmail.com>
|
||||
Jeremy Danyow <jedanyow@microsoft.com>
|
||||
Jianhui Zhao <jianhuizhao329@gmail.com>
|
||||
Joao Moreno <jomo@microsoft.com>
|
||||
Joao Moreno <mail@joaomoreno.com>
|
||||
Johannes Zellner <johannes@nebulon.de>
|
||||
Jon Austin <jon.i.austin@gmail.com>
|
||||
Jon Kohler <jon@nutanix.com>
|
||||
Jon Masters <jon.masters@sky.com>
|
||||
Jörg Breitbart <jerch@rockborn.de>
|
||||
Jose Anton <roke22@gmail.com>
|
||||
jpoth <poth.john@gmail.com>
|
||||
Justin Luk <jluk@users.noreply.github.com>
|
||||
Justin Mecham <justin@mecham.me>
|
||||
Kirill Merkushev <lanwen@yandex.ru>
|
||||
Krasimir Tsonev <krasimir@outset.ws>
|
||||
Ledion Bitincka <lbitincka@gmail.com>
|
||||
Linus Unnebäck <linus@folkdatorn.se>
|
||||
Luca <LucaT1@users.noreply.github.com>
|
||||
Lucian Buzzo <lucian.buzzo@gmail.com>
|
||||
Lukas Drgon <lukas.drgon@gmail.com>
|
||||
Lukas Geiger <lukas.geiger94@gmail.com>
|
||||
Maël Nison <nison.mael@gmail.com>
|
||||
Marc Dumais <marc.dumais@ericsson.com>
|
||||
Marek Libra <mlibra@redhat.com>
|
||||
Markus F.X.J. Oberhumer <markus@oberhumer.com>
|
||||
Martin Chloride <i@martincl2.me>
|
||||
Martin Koppehel <martin.koppehel@st.ovgu.de>
|
||||
Martin Wang <jiahaow@ca.ibm.com>
|
||||
Matt Bierner <matb@microsoft.com>
|
||||
Matthew James <matthew.d.james87@gmail.com>
|
||||
Michael Irwin <mikesir87@gmail.com>
|
||||
Mikko Karvonen <mikko.karvonen@arm.com>
|
||||
mofux <t.zilz@mofux.org>
|
||||
muji <muji@tmpfs.org>
|
||||
Nicolas Ramz <nicolas.ramz@gmail.com>
|
||||
npezza93 <npezza93@gmail.com>
|
||||
Oleksandr Andriienko <oandriie@redhat.com>
|
||||
Paris Kasidiaris <pariskasidiaris@gmail.com>
|
||||
Paris Kasidiaris <paris@sourcelair.com>
|
||||
Peng Xiao <pengxiao@outlook.com>
|
||||
Peter Baumgarten <me@peterbaumgarten.com>
|
||||
Philip Olson <philip.olson@protonmail.ch>
|
||||
PowerHat <31401273+7PH@users.noreply.github.com>
|
||||
PowerHat <b.raymond@protonmail.com>
|
||||
pro-src <34285059+pro-src@users.noreply.github.com>
|
||||
pro-src <rodneyd.teal@gmail.com>
|
||||
Rick Baker <rick@ricktbaker.com>
|
||||
runarberg <runar@greenqloud.com>
|
||||
Saad Malik <simfox3@gmail.com>
|
||||
Samuel Williams <samuel.williams@oriontransfer.co.nz>
|
||||
Saswat Das <saswatds@users.noreply.github.com>
|
||||
Saul Costa <saul@codevolve.com>
|
||||
Segev Finer <segev208@gmail.com>
|
||||
Shuanglei Tao <tsl0922@gmail.com>
|
||||
sitzmar <sitzmar@gmail.com>
|
||||
Steven Silvester <steven.silvester@ieee.org>
|
||||
stuicey <stuicey@users.noreply.github.com>
|
||||
Sven-Hendrik Haase <svenstaro@gmail.com>
|
||||
t-amqi <umberos@gmail.com>
|
||||
Thanasis Daglis <thanasis@sourcelair.com>
|
||||
thomas <thomas@kreativkonzentrat.de>
|
||||
@@ -100,6 +127,8 @@ Thomas Zilz <t.zilz@mofux.org>
|
||||
Tine Jozelj <tine.jozelj@outlook.com>
|
||||
Tyler Jewell <tjewell@codenvy.com>
|
||||
Vincent Woo <me@vincentwoo.com>
|
||||
Viraj Sinha <virajosinha@gmail.com>
|
||||
yutaka <shigure@refy.net>
|
||||
YuviPanda <yuvipanda@gmail.com>
|
||||
ZHAO Xudong <zxdong@gmail.com>
|
||||
赵雪珂 <zhaoxueke@onething.net>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age, body
|
||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
||||
education, socio-economic status, nationality, personal appearance, race,
|
||||
religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behavior and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event. Representation of a project may be
|
||||
further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting [@Tyriar](https://twitter.com/Tyriar) or [@pariskasid](https://twitter.com/pariskasid) on Twitter. All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
+2
-1
@@ -23,7 +23,7 @@ opening an issue, read these pointers.
|
||||
- Mention precisely what went wrong. What did you expect to happen? What happened instead? Describe the
|
||||
exact steps a maintainer has to take to make the problem occur.
|
||||
|
||||
- If the problem can not be reproduced in the [demo of xterm.js](README.md#demo), please provide an HTML document that demonstrates the problem.
|
||||
- If the problem can not be reproduced in the [demo of xterm.js](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo), please provide an HTML document that demonstrates the problem.
|
||||
|
||||
- Be polite. Issues with an indignant or belligerent tone tend to be moved to the
|
||||
bottom of the pile.
|
||||
@@ -33,6 +33,7 @@ opening an issue, read these pointers.
|
||||
- Make sure you have a [GitHub account](https://github.com/join)
|
||||
- Fork [xterm.js](https://github.com/sourcelair/xterm.js/)
|
||||
([how to fork a repo](https://help.github.com/articles/fork-a-repo))
|
||||
- Get the [xterm.js demo](https://github.com/xtermjs/xterm.js/wiki/Contributing#running-the-demo) running
|
||||
- Make your changes
|
||||
- If your changes are easy to test or likely to regress, add tests. Tests go into `test`, directory.
|
||||
- Follow the general code style of the rest of the project (see below).
|
||||
|
||||
+1
-1
@@ -16,4 +16,4 @@ RUN npm install
|
||||
COPY . /usr/src/app
|
||||
|
||||
# Run the tests and build, to make sure everything is working nicely
|
||||
RUN npm run build && npm run test
|
||||
RUN npm run build && npm run webpack && npm run test
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Copyright (c) 2017-2018, The xterm.js authors (https://github.com/xtermjs/xterm.js)
|
||||
Copyright (c) 2014-2016, SourceLair Private Company (https://www.sourcelair.com)
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (https://github.com/chjj/)
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# [](https://xtermjs.org)
|
||||
|
||||
[](https://travis-ci.org/xtermjs/xterm.js) [](https://coveralls.io/github/sourcelair/xterm.js) [](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [](https://www.jsdelivr.com/package/npm/xterm)
|
||||
[](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1)
|
||||
[](https://coveralls.io/github/xtermjs/xterm.js?branch=master)
|
||||
[](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](https://www.jsdelivr.com/package/npm/xterm)
|
||||
|
||||
Xterm.js is a terminal front-end component written in JavaScript that works in the browser.
|
||||
|
||||
@@ -39,9 +42,9 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
|
||||
<body>
|
||||
<div id="terminal"></div>
|
||||
<script>
|
||||
var term = new Terminal();
|
||||
var term = new Terminal();
|
||||
term.open(document.getElementById('terminal'));
|
||||
term.write('Hello from \033[1;3;31mxterm.js\033[0m $ ')
|
||||
term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -57,7 +60,11 @@ The proposed way to load xterm.js is via the ES6 module syntax.
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
*Note: There are currently no typings for addons so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.*
|
||||
### API
|
||||
|
||||
The full API for xterm.js is contained within the [TypeScript declaration file](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), use the branch/tag picker in GitHub (`w`) to navigate to the correct version of the API.
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added so we can experiment with new ideas without committing to support it like a normal semver API. Note that these APIs can change radically between versions so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
### Addons
|
||||
|
||||
@@ -76,6 +83,29 @@ var xterm = new Terminal(); // Instantiate the terminal
|
||||
xterm.fit(); // Use the `fit` method, provided by the `fit` addon
|
||||
```
|
||||
|
||||
You will also need to include the addon's CSS file if it has one in the folder.
|
||||
|
||||
#### Importing Addons in TypeScript
|
||||
|
||||
There are currently no typings for addons if they are accessed via extending Terminal prototype, so you will need to upcast if using TypeScript, eg. `(<any>xterm).fit()`.
|
||||
|
||||
Alternatively, you can import addon function and enhance the terminal on demand. This would have better typing support and is friendly to treeshaking. E.g.:
|
||||
|
||||
```typescript
|
||||
import { Terminal } from 'xterm';
|
||||
import { fit } from 'xterm/lib/addons/fit/fit';
|
||||
const xterm = new Terminal();
|
||||
|
||||
// Fit the terminal when necessary:
|
||||
fit(xterm);
|
||||
```
|
||||
|
||||
#### Third party addons
|
||||
|
||||
There are also the following third party addons available:
|
||||
|
||||
- [xterm-webfont](https://www.npmjs.com/package/xterm-webfont)
|
||||
|
||||
## Browser Support
|
||||
|
||||
Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Here is a list of the versions we aim to support:
|
||||
@@ -88,6 +118,10 @@ Since xterm.js is typically implemented as a developer tool, only modern browser
|
||||
|
||||
Xterm.js works seamlessly in Electron apps and may even work on earlier versions of the browsers but these are the browsers we strive to keep working.
|
||||
|
||||
## API
|
||||
|
||||
The current full API documentation is available in the [TypeScript declaration file on the repository](https://github.com/xtermjs/xterm.js/blob/master/typings/xterm.d.ts), switch the tag (press `w` when viewing the file) to point at the specific version tag you're using.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
@@ -110,7 +144,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Selenoid UI**](https://github.com/aerokube/selenoid-ui): Simple UI for the scallable golang implementation of Selenium Hub named Selenoid. We use XTerm for streaming logs over websockets from docker containers.
|
||||
- [**Portainer**](https://portainer.io): Simple management UI for Docker.
|
||||
- [**SSHy**](https://github.com/stuicey/SSHy): HTML5 Based SSHv2 Web Client with E2E encryption utilising `xterm.js`, SJCL & websockets.
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
- [**JupyterLab**](https://github.com/jupyterlab/jupyterlab): An extensible
|
||||
computational environment for Jupyter, supporting interactive data science and scientific computing across all programming languages.
|
||||
- [**Theia**](https://github.com/theia-ide/theia): Theia is a cloud & desktop IDE framework implemented in TypeScript.
|
||||
- [**Opshell**](https://github.com/ricktbaker/opshell) Ops Helper tool to make life easier working with AWS instances across multiple organizations.
|
||||
@@ -126,67 +160,20 @@ computational environment for Jupyter, supporting interactive data science and s
|
||||
- [**Pisth**](https://github.com/ColdGrub1384/Pisth): An SFTP and SSH client for iOS
|
||||
- [**abstruse**](https://github.com/bleenco/abstruse): Abstruse CI is a continuous integration platform based on Node.JS and Docker.
|
||||
- [**Microsoft SQL Operations Studio**](https://github.com/Microsoft/sqlopsstudio): A data management tool that enables working with SQL Server, Azure SQL DB and SQL DW from Windows, macOS and Linux
|
||||
- [**FreeMAN**](https://github.com/matthew-matvei/freeman): A free, cross-platform file manager for power users
|
||||
- [**Fluent Terminal**](https://github.com/felixse/FluentTerminal): A terminal emulator based on UWP and web technologies.
|
||||
- [**Hyper**](https://hyper.is): A terminal built on web technologies
|
||||
- [**Diag**](https://diag.ai): A better way to troubleshoot problems faster. Capture, share and reapply troubleshooting knowledge so you can focus on solving problems that matter.
|
||||
- [**GoTTY**](https://github.com/yudai/gotty): A simple command line tool that shares your terminal as a web application based on xterm.js.
|
||||
- [**genact**](https://github.com/svenstaro/genact): A nonsense activity generator.
|
||||
- [**cPanel & WHM**](https://cpanel.com): The hosting platform of choice.
|
||||
- [**Nutanix**](https://github.com/nutanix): Nutanix Enterprise Cloud uses xterm in the webssh functionality within Nutanix Calm, and is also looking to move our old noserial (termjs) functionality to xterm.js
|
||||
- [**SSH Web Client**](https://github.com/roke22/PHP-SSH2-Web-Client): SSH Web Client with PHP.
|
||||
- [**Shellvault**](https://www.shellvault.io): The cloud-based SSH terminal you can access from anywhere.
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list.
|
||||
[And much more...](https://github.com/xtermjs/xterm.js/network/dependents)
|
||||
|
||||
## Demo
|
||||
|
||||
Xterm.js ships with a barebones demo implementation, designed for the development and evaluation of the library only. Exposing the demo to the public as is would introduce security risks for the host.
|
||||
|
||||
Below you can find instructions on how to run the demo on different platforms.
|
||||
|
||||
### SourceLair
|
||||
|
||||
SourceLair will run the demo and builder in parallel automatically. Just make sure to choose the "Node.js" project type, when cloning the xterm.js repo (or just use this shortcut; https://lair.io/xtermjs/xtermjs).
|
||||
|
||||
Then open your project's [Public URL](https://help.sourcelair.com/projects/the-public-url/) to access the demo.
|
||||
|
||||
### Docker
|
||||
|
||||
First, make sure you have Docker Engine 1.13.0 (or newer) and Docker Compose 1.10.0 (or newer). To run the demo and builder in parallel, run the following command in your terminal:
|
||||
|
||||
```
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo. If you prefer a different port than `3000` to access the xterm.js demo, then set the `XTERMJS_PORT` environment variable to the desired port.
|
||||
|
||||
### Foreman (or other Procfile runner)
|
||||
|
||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
foreman start # Replace foreman with "honcho", "forego" etc. depending on your runner
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
### Linux or macOS
|
||||
|
||||
First, be sure that a C++ compiler such as GCC-C++ or Clang is installed, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
### Windows
|
||||
|
||||
First, ensure [node-gyp](https://github.com/nodejs/node-gyp) is installed and configured correctly, then run the following commands in your terminal:
|
||||
|
||||
```
|
||||
npm install
|
||||
npm start # Run this in its own terminal
|
||||
npm run watch # Run this in its own terminal
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:3000 in a web browser to access the demo.
|
||||
|
||||
*Note: Do not use ConEmu, as it seems to break the demo for some reason.*
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it in our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
@@ -194,20 +181,14 @@ Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
The existing releases are available at this GitHub repo's [Releases](https://github.com/sourcelair/xterm.js/releases), while the roadmap is available as [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
## Development and Contribution
|
||||
## Contributing
|
||||
|
||||
Xterm.js is maintained by [SourceLair](https://www.sourcelair.com/) and a few external contributors, but we would love to receive contributions from everyone!
|
||||
|
||||
To contribute either code, documentation or issues to xterm.js please read the [Contributing document](CONTRIBUTING.md) beforehand. The development of xterm.js does not require any special tool. All you need is an editor that supports JavaScript/TypeScript and a browser. You will need Node.js installed locally to get all the features working in the demo.
|
||||
|
||||
### Code structure
|
||||
|
||||
`src/` is roughly split up into areas of functionality such as `renderer/` that handles all rendering and `utils/` which provides general utility functions. The `shared/` folder contains code that can be used from either the main thread or a web worker thread, all code inside a `shared/` folder should only ever import other code from a `shared/` folder to minimize the amount of code run what launching a web worker.
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and setup xterm.js for development.
|
||||
|
||||
## License Agreement
|
||||
|
||||
If you contribute code to this project, you are implicitly allowing your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)
|
||||
Copyright (c) 2017-2018, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
+35
-74
@@ -1,77 +1,38 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>xterm.js: xterm, in the browser</h1>
|
||||
<div id="terminal-container"></div>
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Options</h2>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-cursor-blink"> cursorBlink</label>
|
||||
</p>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-mac-option-is-meta"> macOptionIsMeta</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
cursorStyle
|
||||
<select id="option-cursor-style">
|
||||
<option value="block">block</option>
|
||||
<option value="underline">underline</option>
|
||||
<option value="bar">bar</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
bellStyle
|
||||
<select id="option-bell-style">
|
||||
<option value="">none</option>
|
||||
<option value="sound">sound</option>
|
||||
<option value="visual">visual</option>
|
||||
<option value="both">both</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>scrollback <input type="number" id="option-scrollback" value="1000" /></label>
|
||||
</p>
|
||||
<p>
|
||||
<label>tabStopWidth <input type="number" id="option-tabstopwidth" value="8" /></label>
|
||||
</p>
|
||||
<div>
|
||||
<h3>Size</h3>
|
||||
<div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="cols">Columns</label>
|
||||
<input type="number" id="cols" />
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="rows">Rows</label>
|
||||
<input type="number" id="rows" />
|
||||
</div>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="number" id="padding" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||
<script src="dist/bundle.js" defer ></script>
|
||||
</body>
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/build/xterm.css" />
|
||||
<link rel="stylesheet" href="/build/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
|
||||
<div id="terminal-container"></div>
|
||||
<div>
|
||||
<h3>Actions</h3>
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Options</h3>
|
||||
<p>These options can be set in the <code>Terminal</code> constructor or using the <code>Terminal.setOption</code> function.</p>
|
||||
<div id="options-container"></div>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Style</h3>
|
||||
<div style="display: inline-block; margin-right: 16px;">
|
||||
<label for="padding">Padding</label>
|
||||
<input type="number" id="padding" />
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
<p><strong>Attention:</strong> The demo is a barebones implementation and is designed for the development and evaluation of xterm.js only. Exposing the demo to the public as is would introduce security risks for the host.</p>
|
||||
<button id="dispose" title="This is used to testing memory leaks">Dispose terminal</button>
|
||||
<script src="dist/bundle.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+149
-68
@@ -3,6 +3,7 @@ import * as attach from '../build/addons/attach/attach';
|
||||
import * as fit from '../build/addons/fit/fit';
|
||||
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
|
||||
import * as search from '../build/addons/search/search';
|
||||
import * as webLinks from '../build/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
|
||||
|
||||
|
||||
@@ -10,6 +11,7 @@ Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
@@ -24,82 +26,38 @@ var terminalContainer = document.getElementById('terminal-container'),
|
||||
findNext: document.querySelector('#find-next'),
|
||||
findPrevious: document.querySelector('#find-previous')
|
||||
},
|
||||
optionElements = {
|
||||
cursorBlink: document.querySelector('#option-cursor-blink'),
|
||||
cursorStyle: document.querySelector('#option-cursor-style'),
|
||||
macOptionIsMeta: document.querySelector('#option-mac-option-is-meta'),
|
||||
scrollback: document.querySelector('#option-scrollback'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth'),
|
||||
bellStyle: document.querySelector('#option-bell-style')
|
||||
},
|
||||
colsElement = document.getElementById('cols'),
|
||||
rowsElement = document.getElementById('rows'),
|
||||
paddingElement = document.getElementById('padding');
|
||||
|
||||
function setTerminalSize() {
|
||||
var cols = parseInt(colsElement.value, 10);
|
||||
var rows = parseInt(rowsElement.value, 10);
|
||||
var width = (cols * term.renderer.dimensions.actualCellWidth + term.viewport.scrollBarWidth).toString() + 'px';
|
||||
var height = (rows * term.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.fit();
|
||||
}
|
||||
|
||||
function setPadding() {
|
||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||
term.fit();
|
||||
}
|
||||
|
||||
colsElement.addEventListener('change', setTerminalSize);
|
||||
rowsElement.addEventListener('change', setTerminalSize);
|
||||
paddingElement.addEventListener('change', setPadding);
|
||||
|
||||
actionElements.findNext.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findNext(actionElements.findNext.value);
|
||||
}
|
||||
});
|
||||
actionElements.findPrevious.addEventListener('keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findPrevious(actionElements.findPrevious.value);
|
||||
}
|
||||
});
|
||||
|
||||
optionElements.cursorBlink.addEventListener('change', function () {
|
||||
term.setOption('cursorBlink', optionElements.cursorBlink.checked);
|
||||
});
|
||||
optionElements.cursorStyle.addEventListener('change', function () {
|
||||
term.setOption('cursorStyle', optionElements.cursorStyle.value);
|
||||
});
|
||||
optionElements.bellStyle.addEventListener('change', function () {
|
||||
term.setOption('bellStyle', optionElements.bellStyle.value);
|
||||
});
|
||||
optionElements.macOptionIsMeta.addEventListener('change', function () {
|
||||
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
|
||||
});
|
||||
optionElements.scrollback.addEventListener('change', function () {
|
||||
term.setOption('scrollback', parseInt(optionElements.scrollback.value, 10));
|
||||
});
|
||||
optionElements.tabstopwidth.addEventListener('change', function () {
|
||||
term.setOption('tabStopWidth', parseInt(optionElements.tabstopwidth.value, 10));
|
||||
});
|
||||
|
||||
createTerminal();
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
// If the terminal exists dispose of it, otherwise recreate it
|
||||
if (term) {
|
||||
term.dispose();
|
||||
term = null;
|
||||
window.term = null;
|
||||
socket = null;
|
||||
document.getElementById('dispose').innerHTML = 'Recreate Terminal';
|
||||
}
|
||||
else {
|
||||
createTerminal();
|
||||
document.getElementById('dispose').innerHTML = 'Dispose terminal';
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
|
||||
|
||||
function createTerminal() {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({
|
||||
macOptionIsMeta: optionElements.macOptionIsMeta.enabled,
|
||||
cursorBlink: optionElements.cursorBlink.checked,
|
||||
scrollback: parseInt(optionElements.scrollback.value, 10),
|
||||
tabStopWidth: parseInt(optionElements.tabstopwidth.value, 10)
|
||||
});
|
||||
term = new Terminal({});
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.on('resize', function (size) {
|
||||
if (!pid) {
|
||||
@@ -116,17 +74,34 @@ function createTerminal() {
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findNext(actionElements.findNext.value);
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keypress', function (e) {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
term.findPrevious(actionElements.findPrevious.value);
|
||||
}
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
initOptions(term);
|
||||
document.getElementById(`opt-cols`).value = term.cols;
|
||||
document.getElementById(`opt-rows`).value = term.rows;
|
||||
paddingElement.value = 0;
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
updateTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
@@ -166,7 +141,7 @@ function runFakeTerminal() {
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term.on('key', function (key, ev) {
|
||||
term._core.register(term.addDisposableListener('key', function (key, ev) {
|
||||
var printable = (
|
||||
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
|
||||
);
|
||||
@@ -181,9 +156,115 @@ function runFakeTerminal() {
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
}
|
||||
}));
|
||||
|
||||
term._core.register(term.addDisposableListener('paste', function (data, ev) {
|
||||
term.write(data);
|
||||
}));
|
||||
}
|
||||
|
||||
function initOptions(term) {
|
||||
var blacklistedOptions = [
|
||||
// Internal only options
|
||||
'cancelEvents',
|
||||
'convertEol',
|
||||
'debug',
|
||||
'handler',
|
||||
'screenKeys',
|
||||
'termName',
|
||||
'useFlowControl',
|
||||
// Complex option
|
||||
'theme'
|
||||
];
|
||||
var stringOptions = {
|
||||
bellSound: null,
|
||||
bellStyle: ['none', 'sound'],
|
||||
cursorStyle: ['block', 'underline', 'bar'],
|
||||
experimentalCharAtlas: ['none', 'static', 'dynamic'],
|
||||
fontFamily: null,
|
||||
fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
|
||||
rendererType: ['dom', 'canvas']
|
||||
};
|
||||
var options = Object.keys(term._core.options);
|
||||
var booleanOptions = [];
|
||||
var numberOptions = [];
|
||||
options.filter(o => blacklistedOptions.indexOf(o) === -1).forEach(o => {
|
||||
switch (typeof term.getOption(o)) {
|
||||
case 'boolean':
|
||||
booleanOptions.push(o);
|
||||
break;
|
||||
case 'number':
|
||||
numberOptions.push(o);
|
||||
break;
|
||||
default:
|
||||
if (Object.keys(stringOptions).indexOf(o) === -1) {
|
||||
console.warn(`Unrecognized option: "${o}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
term.on('paste', function (data, ev) {
|
||||
term.write(data);
|
||||
var html = '';
|
||||
html += '<div class="option-group">';
|
||||
booleanOptions.forEach(o => {
|
||||
html += `<div class="option"><label><input id="opt-${o}" type="checkbox" ${term.getOption(o) ? 'checked' : ''}/> ${o}</label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
numberOptions.forEach(o => {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="number" value="${term.getOption(o)}"/></label></div>`;
|
||||
});
|
||||
html += '</div><div class="option-group">';
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
if (stringOptions[o]) {
|
||||
html += `<div class="option"><label>${o} <select id="opt-${o}">${stringOptions[o].map(v => `<option ${term.getOption(o) === v ? 'selected' : ''}>${v}</option>`).join('')}</select></label></div>`;
|
||||
} else {
|
||||
html += `<div class="option"><label>${o} <input id="opt-${o}" type="text" value="${term.getOption(o)}"/></label></div>`
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
var container = document.getElementById('options-container');
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach listeners
|
||||
booleanOptions.forEach(o => {
|
||||
var input = document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.checked);
|
||||
term.setOption(o, input.checked);
|
||||
});
|
||||
});
|
||||
numberOptions.forEach(o => {
|
||||
var input = document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'cols' || o === 'rows') {
|
||||
updateTerminalSize();
|
||||
} else {
|
||||
term.setOption(o, parseInt(input.value, 10));
|
||||
}
|
||||
});
|
||||
});
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
var input = document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
term.setOption(o, input.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addDomListener(element, type, handler) {
|
||||
element.addEventListener(type, handler);
|
||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
}
|
||||
|
||||
function updateTerminalSize() {
|
||||
var cols = parseInt(document.getElementById(`opt-cols`).value, 10);
|
||||
var rows = parseInt(document.getElementById(`opt-rows`).value, 10);
|
||||
var width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
var height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.fit();
|
||||
}
|
||||
|
||||
@@ -14,3 +14,19 @@ h1 {
|
||||
margin: 0 auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 0.9em;
|
||||
font-style: italic
|
||||
}
|
||||
|
||||
#option-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.option-group {
|
||||
display: inline-block;
|
||||
padding-left: 20px;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
@@ -7,16 +7,16 @@ var pty = require('node-pty');
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
|
||||
app.use('/build', express.static(__dirname + '/../../../../build'));
|
||||
app.use('/demo', express.static(__dirname + '/../../../../demo'));
|
||||
app.use('/zmodemjs', express.static(__dirname + '/../../../../node_modules/zmodem.js/dist'));
|
||||
app.use('/build', express.static(__dirname + '/../../build'));
|
||||
app.use('/demo', express.static(__dirname + '/../../demo'));
|
||||
app.use('/zmodemjs', express.static(__dirname + '/../../node_modules/zmodem.js/dist'));
|
||||
|
||||
app.get('/', function(req, res){
|
||||
res.sendFile(__dirname + '/index.html');
|
||||
});
|
||||
|
||||
app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
res.sendFile(__dirname + '../style.css');
|
||||
});
|
||||
|
||||
app.get('/main.js', function(req, res){
|
||||
@@ -6,6 +6,11 @@ var term,
|
||||
socket,
|
||||
pid;
|
||||
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(zmodem);
|
||||
Terminal.applyAddon(search);
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
findNext: document.querySelector('#find-next'),
|
||||
@@ -2,6 +2,7 @@ version: "3"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: xtermjs/xterm.js:latest
|
||||
build: .
|
||||
volumes:
|
||||
- ./:/usr/src/app
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5"
|
||||
"target": "es5",
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
||||
+25
-81
@@ -4,11 +4,9 @@
|
||||
|
||||
const browserify = require('browserify');
|
||||
const buffer = require('vinyl-buffer');
|
||||
const coveralls = require('gulp-coveralls');
|
||||
const fs = require('fs-extra');
|
||||
const gulp = require('gulp');
|
||||
const path = require('path');
|
||||
const istanbul = require('gulp-istanbul');
|
||||
const merge = require('merge-stream');
|
||||
const mocha = require('gulp-mocha');
|
||||
const sorcery = require('sorcery');
|
||||
@@ -20,10 +18,17 @@ const webpack = require('webpack-stream');
|
||||
|
||||
const buildDir = process.env.BUILD_DIR || 'build';
|
||||
const tsProject = ts.createProject('tsconfig.json');
|
||||
const srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let srcDir = tsProject.config.compilerOptions.rootDir;
|
||||
let outDir = tsProject.config.compilerOptions.outDir;
|
||||
|
||||
const addons = ['attach', 'fit', 'fullscreen', 'search', 'terminado', 'winptyCompat', 'zmodem'];
|
||||
const addons = fs.readdirSync(`${__dirname}/src/addons`);
|
||||
|
||||
const TEST_PATHS = [
|
||||
`${outDir}/*test.js`,
|
||||
`${outDir}/**/*test.js`,
|
||||
`${outDir}/*integration.js`,
|
||||
`${outDir}/**/*integration.js`
|
||||
];
|
||||
|
||||
// Under some environments like TravisCI, this comes out at absolute which can
|
||||
// break the build. This ensures that the outDir is absolute.
|
||||
@@ -31,58 +36,19 @@ if (path.normalize(outDir).indexOf(__dirname) !== 0) {
|
||||
outDir = `${__dirname}/${path.normalize(outDir)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile TypeScript sources to JavaScript files and create a source map file for each TypeScript
|
||||
* file compiled.
|
||||
*/
|
||||
gulp.task('tsc', function () {
|
||||
// Remove the ${outDir}/ directory to prevent confusion if files were deleted in ${srcDir}/
|
||||
fs.emptyDirSync(`${outDir}`);
|
||||
gulp.task('css', function() {
|
||||
return gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
|
||||
});
|
||||
|
||||
// Build all TypeScript files (including tests) to ${outDir}/, based on the configuration defined in
|
||||
// `tsconfig.json`.
|
||||
let tsResult = tsProject.src().pipe(sourcemaps.init()).pipe(tsProject());
|
||||
let tsc = merge(
|
||||
tsResult.js.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir)),
|
||||
tsResult.dts.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''})).pipe(gulp.dest(outDir))
|
||||
);
|
||||
|
||||
let addonStreams = addons.map(function(addon) {
|
||||
fs.emptyDirSync(`${outDir}/addons/${addon}`);
|
||||
|
||||
let tsProjectAddon = ts.createProject(`./src/addons/${addon}/tsconfig.json`);
|
||||
let tsResultAddon = tsProjectAddon.src().pipe(sourcemaps.init()).pipe(tsProjectAddon());
|
||||
let tscAddon = merge(
|
||||
tsResultAddon.js
|
||||
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
|
||||
.pipe(gulp.dest(`${outDir}/addons/${addon}`)),
|
||||
tsResultAddon.dts
|
||||
.pipe(sourcemaps.write('.', {includeContent: false, sourceRoot: ''}))
|
||||
.pipe(gulp.dest(`${outDir}/addons/${addon}`))
|
||||
)
|
||||
|
||||
return tscAddon;
|
||||
});
|
||||
|
||||
// Copy all addons from ${srcDir}/ to ${outDir}/
|
||||
let copyAddons = gulp.src([
|
||||
`${srcDir}/addons/**/**`
|
||||
]).pipe(gulp.dest(`${outDir}/addons`));
|
||||
|
||||
// Copy stylesheets from ${srcDir}/ to ${outDir}/
|
||||
let copyStylesheets = gulp.src(`${srcDir}/**/*.css`).pipe(gulp.dest(outDir));
|
||||
|
||||
// Join all streams into a single array
|
||||
let streams = [tsc].concat(addonStreams).concat([copyAddons, copyStylesheets]);
|
||||
|
||||
return merge.apply(this, streams);
|
||||
gulp.task('watch-css', function() {
|
||||
return gulp.watch(`${srcDir}/**/*.css`, ['css']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Bundle JavaScript files produced by the `tsc` task, into a single file named `xterm.js` with
|
||||
* Browserify.
|
||||
*/
|
||||
gulp.task('browserify', ['tsc'], function() {
|
||||
gulp.task('browserify', function() {
|
||||
// Ensure that the build directory exists
|
||||
fs.ensureDirSync(buildDir);
|
||||
|
||||
@@ -108,7 +74,7 @@ gulp.task('browserify', ['tsc'], function() {
|
||||
return merge(bundleStream, copyStylesheets);
|
||||
});
|
||||
|
||||
gulp.task('browserify-addons', ['tsc'], function() {
|
||||
gulp.task('browserify-addons', function() {
|
||||
const bundles = addons.map((addon) => {
|
||||
const addonOptions = {
|
||||
basedir: `${buildDir}/addons/${addon}`,
|
||||
@@ -134,37 +100,22 @@ gulp.task('browserify-addons', ['tsc'], function() {
|
||||
return merge(...bundles);
|
||||
});
|
||||
|
||||
gulp.task('instrument-test', function () {
|
||||
return gulp.src([`${outDir}/**/*.js`])
|
||||
// Covering files
|
||||
.pipe(istanbul())
|
||||
// Force `require` to return covered files
|
||||
.pipe(istanbul.hookRequire());
|
||||
});
|
||||
|
||||
gulp.task('mocha', ['instrument-test'], function () {
|
||||
return gulp.src([
|
||||
`${outDir}/*test.js`,
|
||||
`${outDir}/**/*test.js`,
|
||||
`${outDir}/*integration.js`,
|
||||
`${outDir}/**/*integration.js`
|
||||
], {read: false})
|
||||
gulp.task('mocha', function () {
|
||||
return gulp.src(TEST_PATHS, {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1))
|
||||
.pipe(istanbul.writeReports());
|
||||
.once('error', () => process.exit(1));
|
||||
});
|
||||
|
||||
/**
|
||||
* Run single test file by file name(without file extension). Example of the command:
|
||||
* gulp mocha-test --test InputHandler.test
|
||||
* Run single test suite (file) by file name (without file extension). Example of the command:
|
||||
* gulp mocha-suite --test InputHandler.test
|
||||
*/
|
||||
gulp.task('mocha-test', ['instrument-test'], function () {
|
||||
gulp.task('mocha-suite', [], function () {
|
||||
let testName = util.env.test;
|
||||
util.log("Run test by Name: " + testName);
|
||||
return gulp.src([`${outDir}/${testName}.js`, `${outDir}/**/${testName}.js`], {read: false})
|
||||
.pipe(mocha())
|
||||
.once('error', () => process.exit(1))
|
||||
.pipe(istanbul.writeReports());
|
||||
.once('error', () => process.exit(1));
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -192,16 +143,9 @@ gulp.task('webpack', ['build'], function() {
|
||||
.pipe(gulp.dest('demo/dist/'));
|
||||
});
|
||||
|
||||
gulp.task('watch', ['webpack'], () => {
|
||||
gulp.watch(['./src/*', './src/**/*'], ['webpack']);
|
||||
});
|
||||
|
||||
/**
|
||||
* Submit coverage results to coveralls.io
|
||||
*/
|
||||
gulp.task('coveralls', function () {
|
||||
gulp.src('coverage/**/lcov.info')
|
||||
.pipe(coveralls());
|
||||
gulp.task('watch-demo', ['webpack'], () => {
|
||||
gulp.watch(['./demo/*', './lib/**/*'], ['webpack']);
|
||||
});
|
||||
|
||||
gulp.task('build', ['sorcery', 'sorcery-addons']);
|
||||
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
{
|
||||
"source": {
|
||||
"include": [
|
||||
"src/"
|
||||
],
|
||||
"exclude": [
|
||||
"src/test/"
|
||||
],
|
||||
"excludePattern": "src/.+\\.test\\.(js|ts)$"
|
||||
},
|
||||
"opts": {
|
||||
"readme": "README.md",
|
||||
"template": "node_modules/docdash",
|
||||
"encoding": "utf8",
|
||||
"destination": "docs/",
|
||||
"recurse": true,
|
||||
"verbose": true
|
||||
},
|
||||
"plugins": [
|
||||
"plugins/markdown"
|
||||
],
|
||||
"templates": {
|
||||
"cleverLinks": false,
|
||||
"monospaceLinks": false
|
||||
}
|
||||
}
|
||||
Generated
-15661
File diff suppressed because it is too large
Load Diff
+26
-52
@@ -1,52 +1,21 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.1.0",
|
||||
"ignore": [
|
||||
"demo",
|
||||
"test",
|
||||
".gitignore"
|
||||
],
|
||||
"main": "lib/Terminal.js",
|
||||
"version": "3.7.0",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"*.js",
|
||||
"*.json",
|
||||
"dist/*.css",
|
||||
"dist/**/*.css",
|
||||
"dist/*.js",
|
||||
"dist/*.js.map",
|
||||
"dist/**/*.js",
|
||||
"dist/**/*.js.map",
|
||||
"lib/*.css",
|
||||
"lib/**/*.css",
|
||||
"lib/*.d.ts",
|
||||
"lib/*.js",
|
||||
"lib/*.js.map",
|
||||
"lib/**/*.d.ts",
|
||||
"lib/**/*.js",
|
||||
"lib/**/*.js.map",
|
||||
"src/*.css",
|
||||
"src/**/*.css",
|
||||
"src/*.js",
|
||||
"src/*.js.map",
|
||||
"src/*.ts",
|
||||
"src/**/*.js",
|
||||
"src/**/*.js.map",
|
||||
"src/**/*.ts",
|
||||
"typings/*.d.ts"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/chai": "^3.4.34",
|
||||
"@types/jsdom": "^11.0.1",
|
||||
"@types/glob": "^5.0.35",
|
||||
"@types/jsdom": "11.0.1",
|
||||
"@types/mocha": "^2.2.33",
|
||||
"@types/node": "^6.0.41",
|
||||
"@types/text-encoding": "0.0.32",
|
||||
"@types/node": "6.0.108",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.0",
|
||||
"docdash": "0.4.0",
|
||||
"concurrently": "^3.5.1",
|
||||
"coveralls": "^3.0.1",
|
||||
"express": "4.13.4",
|
||||
"express-ws": "2.0.0-rc.1",
|
||||
"fs-extra": "^1.0.0",
|
||||
@@ -54,20 +23,22 @@
|
||||
"gulp": "3.9.1",
|
||||
"gulp-cli": "^1.2.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-coveralls": "^0.1.4",
|
||||
"gulp-istanbul": "^1.1.1",
|
||||
"gulp-mocha": "^3.0.1",
|
||||
"gulp-sourcemaps": "1.9.1",
|
||||
"gulp-typescript": "^3.1.3",
|
||||
"gulp-util": "3.0.8",
|
||||
"jsdoc": "3.4.3",
|
||||
"jsdom": "^11.1.0",
|
||||
"jsdom": "^11.11.0",
|
||||
"merge-stream": "^1.0.1",
|
||||
"node-pty": "^0.7.2",
|
||||
"node-pty": "0.7.6",
|
||||
"nodemon": "1.10.2",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"nyc": "^11.8.0",
|
||||
"sorcery": "^0.10.0",
|
||||
"source-map-loader": "^0.2.3",
|
||||
"tslint": "^5.9.1",
|
||||
"typescript": "~2.7.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "2.8.3",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
@@ -75,18 +46,21 @@
|
||||
"zmodem.js": "^0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node demo/app",
|
||||
"prestart-zmodem": "npm run build",
|
||||
"start-zmodem": "node build/addons/zmodem/demo/app",
|
||||
"lint": "tslint src/*.ts src/**/*.ts",
|
||||
"test": "gulp test",
|
||||
"start": "concurrently --kill-others-on-fail --names \"demo,server\" \"gulp watch-demo\" \"node demo/app\"",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts'",
|
||||
"test": "npm-run-all mocha lint",
|
||||
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
|
||||
"test-suite": "gulp mocha-suite --test",
|
||||
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
|
||||
"mocha": "gulp test",
|
||||
"build:docs": "jsdoc -c jsdoc.json",
|
||||
"tsc": "tsc",
|
||||
"prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"",
|
||||
"build": "gulp build",
|
||||
"prepublish": "npm run build",
|
||||
"coveralls": "gulp coveralls",
|
||||
"coveralls": "nyc report --reporter=text-lcov | coveralls",
|
||||
"webpack": "gulp webpack",
|
||||
"watch": "gulp watch"
|
||||
},
|
||||
"dependencies": {}
|
||||
"watch": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc -w\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\" \"gulp watch-css\""
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './shared/utils/Browser';
|
||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
const enum BoundaryPosition {
|
||||
TOP,
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
export class AccessibilityManager extends Disposable {
|
||||
private _accessibilityTreeRoot: HTMLElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[];
|
||||
private _liveRegion: HTMLElement;
|
||||
private _liveRegionLineCount: number = 0;
|
||||
|
||||
private _renderRowsDebouncer: RenderDebouncer;
|
||||
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
/**
|
||||
* This queue has a character pushed to it for keys that are pressed, if the
|
||||
* next character added to the terminal is equal to the key char then it is
|
||||
* not announced (added to live region) because it has already been announced
|
||||
* by the textarea event (which cannot be canceled). There are some race
|
||||
* condition cases if there is typing while data is streaming, but this covers
|
||||
* the main case of typing into the prompt and inputting the answer to a
|
||||
* question (Y/N, etc.).
|
||||
*/
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
|
||||
this._rowContainer = document.createElement('div');
|
||||
this._rowContainer.classList.add('xterm-accessibility-tree');
|
||||
this._rowElements = [];
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
this._accessibilityTreeRoot.appendChild(this._rowContainer);
|
||||
|
||||
this._renderRowsDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._refreshRows();
|
||||
|
||||
this._liveRegion = document.createElement('div');
|
||||
this._liveRegion.classList.add('live-region');
|
||||
this._liveRegion.setAttribute('aria-live', 'assertive');
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.addDisposableListener('resize', data => this._onResize(data.rows)));
|
||||
this.register(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this.register(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
|
||||
this.register(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
|
||||
this.register(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
|
||||
this.register(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
|
||||
// TODO: Maybe renderer should fire an event on terminal when the characters change and that
|
||||
// should be listened to instead? That would mean that the order of events are always
|
||||
// guarenteed
|
||||
this.register(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
||||
this.register(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the dprchange event isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._terminal.element.removeChild(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = <HTMLElement>e.target;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
// Don't scroll if the buffer top has reached the end in that direction
|
||||
const posInSet = boundaryElement.getAttribute('aria-posinset');
|
||||
const lastRowPos = position === BoundaryPosition.TOP ? '1' : `${this._terminal.buffer.lines.length}`;
|
||||
if (posInSet === lastRowPos) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't scroll when the last focused item was not the second row (focus is going the other
|
||||
// direction)
|
||||
if (e.relatedTarget !== beforeBoundaryElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old boundary element from array
|
||||
let topBoundaryElement: HTMLElement;
|
||||
let bottomBoundaryElement: HTMLElement;
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
topBoundaryElement = boundaryElement;
|
||||
bottomBoundaryElement = this._rowElements.pop()!;
|
||||
this._rowContainer.removeChild(bottomBoundaryElement);
|
||||
} else {
|
||||
topBoundaryElement = this._rowElements.shift()!;
|
||||
bottomBoundaryElement = boundaryElement;
|
||||
this._rowContainer.removeChild(topBoundaryElement);
|
||||
}
|
||||
|
||||
// Remove listeners from old boundary elements
|
||||
topBoundaryElement.removeEventListener('focus', this._topBoundaryFocusListener);
|
||||
bottomBoundaryElement.removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Add new element to array/DOM
|
||||
if (position === BoundaryPosition.TOP) {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.unshift(newElement);
|
||||
this._rowContainer.insertAdjacentElement('afterbegin', newElement);
|
||||
} else {
|
||||
const newElement = this._createAccessibilityTreeNode();
|
||||
this._rowElements.push(newElement);
|
||||
this._rowContainer.appendChild(newElement);
|
||||
}
|
||||
|
||||
// Add listeners to new boundary elements
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Scroll up
|
||||
this._terminal.scrollLines(position === BoundaryPosition.TOP ? -1 : 1);
|
||||
|
||||
// Focus new boundary before element
|
||||
this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2].focus();
|
||||
|
||||
// Prevent the standard behavior
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
// Grow rows as required
|
||||
for (let i = this._rowContainer.children.length; i < this._terminal.rows; i++) {
|
||||
this._rowElements[i] = this._createAccessibilityTreeNode();
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
// Shrink rows as required
|
||||
while (this._rowElements.length > rows) {
|
||||
this._rowContainer.removeChild(this._rowElements.pop()!);
|
||||
}
|
||||
|
||||
// Add bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
private _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
this._refreshRowDimensions(element);
|
||||
return element;
|
||||
}
|
||||
|
||||
private _onTab(spaceCount: number): void {
|
||||
for (let i = 0; i < spaceCount; i++) {
|
||||
this._onChar(' ');
|
||||
}
|
||||
}
|
||||
|
||||
private _onChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
const shiftedChar = this._charsToConsume.shift();
|
||||
if (shiftedChar !== char) {
|
||||
this._announceCharacter(char);
|
||||
}
|
||||
} else {
|
||||
this._announceCharacter(char);
|
||||
}
|
||||
|
||||
if (char === '\n') {
|
||||
this._liveRegionLineCount++;
|
||||
if (this._liveRegionLineCount === MAX_ROWS_TO_READ + 1) {
|
||||
this._liveRegion.textContent += Strings.tooMuchOutput;
|
||||
}
|
||||
}
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.textContent && this._liveRegion.textContent.length > 0 && !this._liveRegion.parentNode) {
|
||||
setTimeout(() => {
|
||||
this._accessibilityTreeRoot.appendChild(this._liveRegion);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _clearLiveRegion(): void {
|
||||
this._liveRegion.textContent = '';
|
||||
this._liveRegionLineCount = 0;
|
||||
|
||||
// Only detach/attach on mac as otherwise messages can go unaccounced
|
||||
if (isMac) {
|
||||
if (this._liveRegion.parentNode) {
|
||||
this._accessibilityTreeRoot.removeChild(this._liveRegion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _onKey(keyChar: string): void {
|
||||
this._clearLiveRegion();
|
||||
this._charsToConsume.push(keyChar);
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
this._renderRowsDebouncer.refresh(start, end);
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
const buffer: IBuffer = this._terminal.buffer;
|
||||
const setSize = buffer.lines.length.toString();
|
||||
for (let i = start; i <= end; i++) {
|
||||
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
|
||||
const posInSet = (buffer.ydisp + i + 1).toString();
|
||||
const element = this._rowElements[i];
|
||||
element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
|
||||
element.setAttribute('aria-posinset', posInSet);
|
||||
element.setAttribute('aria-setsize', setSize);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._terminal.renderer.dimensions.actualCellHeight) {
|
||||
return;
|
||||
}
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._onResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._terminal.renderer.dimensions.actualCellHeight}px`;
|
||||
}
|
||||
|
||||
private _announceCharacter(char: string): void {
|
||||
if (char === ' ') {
|
||||
// Always use nbsp for spaces in order to preserve the space between characters in
|
||||
// voiceover's caption window
|
||||
this._liveRegion.innerHTML += ' ';
|
||||
} else {
|
||||
this._liveRegion.textContent += char;
|
||||
}
|
||||
}
|
||||
}
|
||||
+169
-10
@@ -5,9 +5,10 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { Buffer, DEFAULT_ATTR } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -36,18 +37,75 @@ describe('Buffer', () => {
|
||||
|
||||
describe('fillViewportRows', () => {
|
||||
it('should fill the buffer with blank lines based on the size of the viewport', () => {
|
||||
const blankLineChar = terminal.blankLine()[0];
|
||||
const blankLineChar = BufferLine.blankLine(terminal.cols, DEFAULT_ATTR).get(0);
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.lines.length, INIT_ROWS);
|
||||
for (let y = 0; y < INIT_ROWS; y++) {
|
||||
assert.equal(buffer.lines.get(y).length, INIT_COLS);
|
||||
for (let x = 0; x < INIT_COLS; x++) {
|
||||
assert.deepEqual(buffer.lines.get(y)[x], blankLineChar);
|
||||
assert.deepEqual(buffer.lines.get(y).get(x), blankLineChar);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getWrappedRangeForLine', () => {
|
||||
describe('non-wrapped', () => {
|
||||
it('should return a single row for the first row', () => {
|
||||
buffer.fillViewportRows();
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 0 });
|
||||
});
|
||||
it('should return a single row for a middle row', () => {
|
||||
buffer.fillViewportRows();
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 12 });
|
||||
});
|
||||
it('should return a single row for the last row', () => {
|
||||
buffer.fillViewportRows();
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 23, last: 23 });
|
||||
});
|
||||
});
|
||||
describe('wrapped', () => {
|
||||
it('should return a range for the first row', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(0), { first: 0, last: 1 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping upwards', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(12).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 11, last: 12 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping downwards', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(13).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 12, last: 13 });
|
||||
});
|
||||
it('should return a range for a middle row wrapping both ways', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(11).isWrapped = true;
|
||||
buffer.lines.get(12).isWrapped = true;
|
||||
buffer.lines.get(13).isWrapped = true;
|
||||
buffer.lines.get(14).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(12), { first: 10, last: 14 });
|
||||
});
|
||||
it('should return a range for the last row', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(23).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 1), { first: 22, last: 23 });
|
||||
});
|
||||
it('should return a range for a row that wraps upward to first row', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(1), { first: 0, last: 1 });
|
||||
});
|
||||
it('should return a range for a row that wraps downward to last row', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(buffer.lines.length - 1).isWrapped = true;
|
||||
assert.deepEqual(buffer.getWrappedRangeForLine(buffer.lines.length - 2), { first: 22, last: 23 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resize', () => {
|
||||
describe('column size is reduced', () => {
|
||||
it('should not trim the data in the buffer', () => {
|
||||
@@ -97,11 +155,11 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS);
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
buffer.fillViewportRows();
|
||||
buffer.lines.get(5)[0][1] = 'a';
|
||||
buffer.lines.get(INIT_ROWS - 1)[0][1] = 'b';
|
||||
buffer.lines.get(5).get(0)[1] = 'a';
|
||||
buffer.lines.get(INIT_ROWS - 1).get(0)[1] = 'b';
|
||||
buffer.resize(INIT_COLS, INIT_ROWS - 5);
|
||||
assert.equal(buffer.lines.get(0)[0][1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5)[0][1], 'b');
|
||||
assert.equal(buffer.lines.get(0).get(0)[1], 'a');
|
||||
assert.equal(buffer.lines.get(INIT_ROWS - 1 - 5).get(0)[1], 'b');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -122,7 +180,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(terminal.blankLine());
|
||||
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -142,7 +200,7 @@ describe('Buffer', () => {
|
||||
buffer.fillViewportRows();
|
||||
// Create 10 extra blank lines
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(terminal.blankLine());
|
||||
buffer.lines.push(BufferLine.blankLine(terminal.cols, DEFAULT_ATTR));
|
||||
}
|
||||
// Set cursor to the bottom of the buffer
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
@@ -188,4 +246,105 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.maxLength, INIT_ROWS / 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addMarker', () => {
|
||||
it('should adjust a marker line when the buffer is trimmed', () => {
|
||||
terminal.options.scrollback = 0;
|
||||
buffer = new Buffer(terminal, true);
|
||||
buffer.fillViewportRows();
|
||||
const marker = buffer.addMarker(buffer.lines.length - 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.line, buffer.lines.length - 2);
|
||||
});
|
||||
it('should dispose of a marker if it is trimmed off the buffer', () => {
|
||||
terminal.options.scrollback = 0;
|
||||
buffer = new Buffer(terminal, true);
|
||||
buffer.fillViewportRows();
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
const marker = buffer.addMarker(0);
|
||||
assert.equal(marker.isDisposed, false);
|
||||
assert.equal(buffer.markers.length, 1);
|
||||
buffer.lines.emit('trim', 1);
|
||||
assert.equal(marker.isDisposed, true);
|
||||
assert.equal(buffer.markers.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe ('translateBufferLineToString', () => {
|
||||
it('should handle selecting a section of ascii text', () => {
|
||||
const line = new BufferLine();
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
line.push([ null, 'b', 1, 'b'.charCodeAt(0)]);
|
||||
line.push([ null, 'c', 1, 'c'.charCodeAt(0)]);
|
||||
line.push([ null, 'd', 1, 'd'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str, 'ab');
|
||||
});
|
||||
|
||||
it('should handle a cut-off double width character by including it', () => {
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '語', 2, 35486 ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '語');
|
||||
});
|
||||
|
||||
it('should handle a zero width character in the middle of the string by not including it', () => {
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '語', 2, '語'.charCodeAt(0) ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str0 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str0, '語');
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str1, '語');
|
||||
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||
assert.equal(str2, '語a');
|
||||
});
|
||||
|
||||
it('should handle single width emojis', () => {
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '😁', 1, '😁'.charCodeAt(0) ]);
|
||||
line.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '😁');
|
||||
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str2, '😁a');
|
||||
});
|
||||
|
||||
it('should handle double width emojis', () => {
|
||||
const line = new BufferLine();
|
||||
line.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line.push([ null, '', 0, null]);
|
||||
buffer.lines.set(0, line);
|
||||
|
||||
const str1 = buffer.translateBufferLineToString(0, true, 0, 1);
|
||||
assert.equal(str1, '😁');
|
||||
|
||||
const str2 = buffer.translateBufferLineToString(0, true, 0, 2);
|
||||
assert.equal(str2, '😁');
|
||||
|
||||
const line2 = new BufferLine();
|
||||
line2.push([ null, '😁', 2, '😁'.charCodeAt(0) ]);
|
||||
line2.push([ null, '', 0, null]);
|
||||
line2.push([ null, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
buffer.lines.set(0, line2);
|
||||
|
||||
const str3 = buffer.translateBufferLineToString(0, true, 0, 3);
|
||||
assert.equal(str3, '😁a');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+95
-33
@@ -3,15 +3,23 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { CharData, ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { IMarker } from 'xterm';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
export const DEFAULT_ATTR = (0 << 18) | (257 << 9) | (256 << 0);
|
||||
export const CHAR_DATA_ATTR_INDEX = 0;
|
||||
export const CHAR_DATA_CHAR_INDEX = 1;
|
||||
export const CHAR_DATA_WIDTH_INDEX = 2;
|
||||
export const CHAR_DATA_CODE_INDEX = 3;
|
||||
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
|
||||
export const NULL_CELL_CHAR = ' ';
|
||||
export const NULL_CELL_WIDTH = 1;
|
||||
export const NULL_CELL_CODE = 32;
|
||||
|
||||
/**
|
||||
* This class represents a terminal buffer (an internal state of the terminal), where the
|
||||
* following information is stored (in high-level):
|
||||
@@ -20,8 +28,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
* - scroll position
|
||||
*/
|
||||
export class Buffer implements IBuffer {
|
||||
private _lines: CircularList<LineData>;
|
||||
|
||||
public lines: CircularList<IBufferLine>;
|
||||
public ydisp: number;
|
||||
public ybase: number;
|
||||
public y: number;
|
||||
@@ -31,6 +38,7 @@ export class Buffer implements IBuffer {
|
||||
public tabs: any;
|
||||
public savedY: number;
|
||||
public savedX: number;
|
||||
public markers: Marker[] = [];
|
||||
|
||||
/**
|
||||
* Create a new Buffer.
|
||||
@@ -45,10 +53,6 @@ export class Buffer implements IBuffer {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public get lines(): CircularList<LineData> {
|
||||
return this._lines;
|
||||
}
|
||||
|
||||
public get hasScrollback(): boolean {
|
||||
return this._hasScrollback && this.lines.maxLength > this._terminal.rows;
|
||||
}
|
||||
@@ -78,10 +82,10 @@ export class Buffer implements IBuffer {
|
||||
* Fills the buffer's viewport with blank lines.
|
||||
*/
|
||||
public fillViewportRows(): void {
|
||||
if (this._lines.length === 0) {
|
||||
if (this.lines.length === 0) {
|
||||
let i = this._terminal.rows;
|
||||
while (i--) {
|
||||
this.lines.push(this._terminal.blankLine());
|
||||
this.lines.push(BufferLine.blankLine(this._terminal.cols, DEFAULT_ATTR));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -94,7 +98,7 @@ export class Buffer implements IBuffer {
|
||||
this.ybase = 0;
|
||||
this.y = 0;
|
||||
this.x = 0;
|
||||
this._lines = new CircularList<LineData>(this._getCorrectBufferLength(this._terminal.rows));
|
||||
this.lines = new CircularList<IBufferLine>(this._getCorrectBufferLength(this._terminal.rows));
|
||||
this.scrollTop = 0;
|
||||
this.scrollBottom = this._terminal.rows - 1;
|
||||
this.setupTabStops();
|
||||
@@ -109,19 +113,19 @@ export class Buffer implements IBuffer {
|
||||
// Increase max length if needed before adjustments to allow space to fill
|
||||
// as required.
|
||||
const newMaxLength = this._getCorrectBufferLength(newRows);
|
||||
if (newMaxLength > this._lines.maxLength) {
|
||||
this._lines.maxLength = newMaxLength;
|
||||
if (newMaxLength > this.lines.maxLength) {
|
||||
this.lines.maxLength = newMaxLength;
|
||||
}
|
||||
|
||||
// The following adjustments should only happen if the buffer has been
|
||||
// initialized/filled.
|
||||
if (this._lines.length > 0) {
|
||||
if (this.lines.length > 0) {
|
||||
// Deal with columns increasing (we don't do anything when columns reduce)
|
||||
if (this._terminal.cols < newCols) {
|
||||
const ch: CharData = [this._terminal.defAttr, ' ', 1, 32]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this._lines.length; i++) {
|
||||
while (this._lines.get(i).length < newCols) {
|
||||
this._lines.get(i).push(ch);
|
||||
const ch: CharData = [DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // does xterm use the default attr?
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
while (this.lines.get(i).length < newCols) {
|
||||
this.lines.get(i).push(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,8 +134,8 @@ export class Buffer implements IBuffer {
|
||||
let addToY = 0;
|
||||
if (this._terminal.rows < newRows) {
|
||||
for (let y = this._terminal.rows; y < newRows; y++) {
|
||||
if (this._lines.length < newRows + this.ybase) {
|
||||
if (this.ybase > 0 && this._lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
if (this.lines.length < newRows + this.ybase) {
|
||||
if (this.ybase > 0 && this.lines.length <= this.ybase + this.y + addToY + 1) {
|
||||
// There is room above the buffer and there are no empty elements below the line,
|
||||
// scroll up
|
||||
this.ybase--;
|
||||
@@ -143,16 +147,16 @@ export class Buffer implements IBuffer {
|
||||
} else {
|
||||
// Add a blank line if there is no buffer left at the top to scroll to, or if there
|
||||
// are blank lines after the cursor
|
||||
this._lines.push(this._terminal.blankLine(undefined, undefined, newCols));
|
||||
this.lines.push(BufferLine.blankLine(newCols, DEFAULT_ATTR));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else { // (this._terminal.rows >= newRows)
|
||||
for (let y = this._terminal.rows; y > newRows; y--) {
|
||||
if (this._lines.length > newRows + this.ybase) {
|
||||
if (this._lines.length > this.ybase + this.y + 1) {
|
||||
if (this.lines.length > newRows + this.ybase) {
|
||||
if (this.lines.length > this.ybase + this.y + 1) {
|
||||
// The line is a blank line below the cursor, remove it
|
||||
this._lines.pop();
|
||||
this.lines.pop();
|
||||
} else {
|
||||
// The line is the cursor, scroll down
|
||||
this.ybase++;
|
||||
@@ -164,15 +168,15 @@ export class Buffer implements IBuffer {
|
||||
|
||||
// Reduce max length if needed after adjustments, this is done after as it
|
||||
// would otherwise cut data from the bottom of the buffer.
|
||||
if (newMaxLength < this._lines.maxLength) {
|
||||
if (newMaxLength < this.lines.maxLength) {
|
||||
// Trim from the top of the buffer and adjust ybase and ydisp.
|
||||
const amountToTrim = this._lines.length - newMaxLength;
|
||||
const amountToTrim = this.lines.length - newMaxLength;
|
||||
if (amountToTrim > 0) {
|
||||
this._lines.trimStart(amountToTrim);
|
||||
this.lines.trimStart(amountToTrim);
|
||||
this.ybase = Math.max(this.ybase - amountToTrim, 0);
|
||||
this.ydisp = Math.max(this.ydisp - amountToTrim, 0);
|
||||
}
|
||||
this._lines.maxLength = newMaxLength;
|
||||
this.lines.maxLength = newMaxLength;
|
||||
}
|
||||
|
||||
// Make sure that the cursor stays on screen
|
||||
@@ -220,7 +224,7 @@ export class Buffer implements IBuffer {
|
||||
let endIndex = endCol;
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i];
|
||||
const char = line.get(i);
|
||||
lineString += char[CHAR_DATA_CHAR_INDEX];
|
||||
// Adjust start and end cols for wide characters if they affect their
|
||||
// column indexes
|
||||
@@ -228,7 +232,7 @@ export class Buffer implements IBuffer {
|
||||
if (startCol >= i) {
|
||||
startIndex--;
|
||||
}
|
||||
if (endCol >= i) {
|
||||
if (endCol > i) {
|
||||
endIndex--;
|
||||
}
|
||||
} else {
|
||||
@@ -261,12 +265,26 @@ export class Buffer implements IBuffer {
|
||||
return lineString.substring(startIndex, endIndex);
|
||||
}
|
||||
|
||||
public getWrappedRangeForLine(y: number): { first: number, last: number } {
|
||||
let first = y;
|
||||
let last = y;
|
||||
// Scan upwards for wrapped lines
|
||||
while (first > 0 && this.lines.get(first).isWrapped) {
|
||||
first--;
|
||||
}
|
||||
// Scan downwards for wrapped lines
|
||||
while (last + 1 < this.lines.length && this.lines.get(last + 1).isWrapped) {
|
||||
last++;
|
||||
}
|
||||
return { first, last };
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the tab stops.
|
||||
* @param i The index to start setting up tab stops from.
|
||||
*/
|
||||
public setupTabStops(i?: number): void {
|
||||
if (i != null) {
|
||||
if (i !== null && i !== undefined) {
|
||||
if (!this.tabs[i]) {
|
||||
i = this.prevStop(i);
|
||||
}
|
||||
@@ -285,7 +303,7 @@ export class Buffer implements IBuffer {
|
||||
* @param x The position to move the cursor to the previous tab stop.
|
||||
*/
|
||||
public prevStop(x?: number): number {
|
||||
if (x == null) {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[--x] && x > 0);
|
||||
@@ -297,10 +315,54 @@ export class Buffer implements IBuffer {
|
||||
* @param x The position to move the cursor one tab stop forward.
|
||||
*/
|
||||
public nextStop(x?: number): number {
|
||||
if (x == null) {
|
||||
if (x === null || x === undefined) {
|
||||
x = this.x;
|
||||
}
|
||||
while (!this.tabs[++x] && x < this._terminal.cols);
|
||||
return x >= this._terminal.cols ? this._terminal.cols - 1 : x < 0 ? 0 : x;
|
||||
}
|
||||
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
marker.register(this.lines.addDisposableListener('trim', amount => {
|
||||
marker.line -= amount;
|
||||
// The marker should be disposed when the line is trimmed from the buffer
|
||||
if (marker.line < 0) {
|
||||
marker.dispose();
|
||||
}
|
||||
}));
|
||||
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
|
||||
return marker;
|
||||
}
|
||||
|
||||
private _removeMarker(marker: Marker): void {
|
||||
// TODO: This could probably be optimized by relying on sort order and trimming the array using .length
|
||||
this.markers.splice(this.markers.indexOf(marker), 1);
|
||||
}
|
||||
}
|
||||
|
||||
export class Marker extends EventEmitter implements IMarker {
|
||||
private static _nextId = 1;
|
||||
|
||||
private _id: number = Marker._nextId++;
|
||||
public isDisposed: boolean = false;
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
constructor(
|
||||
public line: number
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
// Emit before super.dispose such that dispose listeners get a change to react
|
||||
this.emit('dispose');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import * as chai from 'chai';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||
|
||||
|
||||
class TestBufferLine extends BufferLine {
|
||||
public toArray(): CharData[] {
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
describe('BufferLine', function(): void {
|
||||
it('ctor', function(): void {
|
||||
let line: IBufferLine = new TestBufferLine();
|
||||
chai.expect(line.length).equals(0);
|
||||
chai.expect(line.pop()).equals(undefined);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
line = new TestBufferLine(10, null, true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.pop()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
line = new TestBufferLine(10, [123, 'a', 456, 789], true);
|
||||
chai.expect(line.length).equals(10);
|
||||
chai.expect(line.pop()).eql([123, 'a', 456, 789]);
|
||||
chai.expect(line.isWrapped).equals(true);
|
||||
});
|
||||
it('splice', function(): void {
|
||||
const line = new TestBufferLine();
|
||||
const data: CharData[] = [
|
||||
[1, 'a', 0, 0],
|
||||
[2, 'b', 0, 0],
|
||||
[3, 'c', 0, 0]
|
||||
];
|
||||
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||
chai.expect(line.length).equals(data.length);
|
||||
const removed1 = line.splice(1, 1, [4, 'd', 0, 0]);
|
||||
const removed2 = data.splice(1, 1, [4, 'd', 0, 0]);
|
||||
chai.expect(removed1).eql(removed2);
|
||||
chai.expect(line.toArray()).eql(data);
|
||||
});
|
||||
it('TerminalLine.blankLine', function(): void {
|
||||
const line = TestBufferLine.blankLine(5, 123);
|
||||
chai.expect(line.length).equals(5);
|
||||
chai.expect(line.isWrapped).equals(false);
|
||||
const ch = line.get(0);
|
||||
chai.expect(ch[CHAR_DATA_ATTR_INDEX]).equals(123);
|
||||
chai.expect(ch[CHAR_DATA_CHAR_INDEX]).equals(NULL_CELL_CHAR);
|
||||
chai.expect(ch[CHAR_DATA_WIDTH_INDEX]).equals(NULL_CELL_WIDTH);
|
||||
chai.expect(ch[CHAR_DATA_CODE_INDEX]).equals(NULL_CELL_CODE);
|
||||
});
|
||||
it('insertCells', function(): void {
|
||||
const line = new TestBufferLine();
|
||||
const data: CharData[] = [
|
||||
[1, 'a', 0, 0],
|
||||
[2, 'b', 0, 0],
|
||||
[3, 'c', 0, 0]
|
||||
];
|
||||
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||
line.insertCells(1, 3, [4, 'd', 0, 0]);
|
||||
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [4, 'd', 0, 0]]);
|
||||
});
|
||||
it('deleteCells', function(): void {
|
||||
const line = new TestBufferLine();
|
||||
const data: CharData[] = [
|
||||
[1, 'a', 0, 0],
|
||||
[2, 'b', 0, 0],
|
||||
[3, 'c', 0, 0],
|
||||
[4, 'd', 0, 0],
|
||||
[5, 'e', 0, 0]
|
||||
];
|
||||
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||
line.deleteCells(1, 2, [6, 'f', 0, 0]);
|
||||
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [4, 'd', 0, 0], [5, 'e', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0]]);
|
||||
});
|
||||
it('replaceCells', function(): void {
|
||||
const line = new TestBufferLine();
|
||||
const data: CharData[] = [
|
||||
[1, 'a', 0, 0],
|
||||
[2, 'b', 0, 0],
|
||||
[3, 'c', 0, 0],
|
||||
[4, 'd', 0, 0],
|
||||
[5, 'e', 0, 0]
|
||||
];
|
||||
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||
line.replaceCells(2, 4, [6, 'f', 0, 0]);
|
||||
chai.expect(line.toArray()).eql([[1, 'a', 0, 0], [2, 'b', 0, 0], [6, 'f', 0, 0], [6, 'f', 0, 0], [5, 'e', 0, 0]]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
import { CharData, IBufferLine } from './Types';
|
||||
import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR } from './Buffer';
|
||||
|
||||
/**
|
||||
* Class representing a terminal line.
|
||||
*/
|
||||
export class BufferLine implements IBufferLine {
|
||||
static blankLine(cols: number, attr: number, isWrapped?: boolean): IBufferLine {
|
||||
const ch: CharData = [attr, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
return new BufferLine(cols, ch, isWrapped);
|
||||
}
|
||||
protected _data: CharData[];
|
||||
public isWrapped = false;
|
||||
public length: number;
|
||||
|
||||
constructor(cols?: number, ch?: CharData, isWrapped?: boolean) {
|
||||
this._data = [];
|
||||
this.length = this._data.length;
|
||||
if (cols) {
|
||||
if (!ch) {
|
||||
ch = [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE];
|
||||
}
|
||||
for (let i = 0; i < cols; i++) {
|
||||
this.push(ch); // Note: the ctor ch is not cloned (resembles old behavior)
|
||||
}
|
||||
}
|
||||
if (isWrapped) {
|
||||
this.isWrapped = true;
|
||||
}
|
||||
}
|
||||
|
||||
public get(index: number): CharData {
|
||||
return this._data[index];
|
||||
}
|
||||
|
||||
public set(index: number, data: CharData): void {
|
||||
this._data[index] = data;
|
||||
}
|
||||
|
||||
public pop(): CharData | undefined {
|
||||
const data = this._data.pop();
|
||||
this.length = this._data.length;
|
||||
return data;
|
||||
}
|
||||
|
||||
public push(data: CharData): void {
|
||||
this._data.push(data);
|
||||
this.length = this._data.length;
|
||||
}
|
||||
|
||||
public splice(start: number, deleteCount: number, ...items: CharData[]): CharData[] {
|
||||
const removed = this._data.splice(start, deleteCount, ...items);
|
||||
this.length = this._data.length;
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** insert n cells ch at pos, right cells are lost (stable length) */
|
||||
public insertCells(pos: number, n: number, ch: CharData): void {
|
||||
while (n--) {
|
||||
this.splice(pos, 0, ch);
|
||||
this.pop();
|
||||
}
|
||||
}
|
||||
|
||||
/** delete n cells at pos, right side is filled with fill (stable length) */
|
||||
public deleteCells(pos: number, n: number, fill: CharData): void {
|
||||
while (n--) {
|
||||
this.splice(pos, 1);
|
||||
this.push(fill);
|
||||
}
|
||||
}
|
||||
|
||||
/** replace cells from pos to pos + n - 1 with fill */
|
||||
public replaceCells(start: number, end: number, fill: CharData): void {
|
||||
while (start < end && start < this.length) {
|
||||
this.set(start++, fill); // Note: fill is not cloned (resembles old behavior)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -18,7 +18,7 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Create a new BufferSet for the given terminal.
|
||||
* @param {Terminal} terminal - The terminal the BufferSet will belong to
|
||||
* @param _terminal - The terminal the BufferSet will belong to
|
||||
*/
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
@@ -35,7 +35,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the alt Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get alt(): Buffer {
|
||||
return this._alt;
|
||||
@@ -43,7 +42,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the normal Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get active(): Buffer {
|
||||
return this._activeBuffer;
|
||||
@@ -51,7 +49,6 @@ export class BufferSet extends EventEmitter implements IBufferSet {
|
||||
|
||||
/**
|
||||
* Returns the currently active Buffer of the BufferSet
|
||||
* @returns {Buffer}
|
||||
*/
|
||||
public get normal(): Buffer {
|
||||
return this._normal;
|
||||
|
||||
+3
-3
@@ -49,7 +49,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
[0x206A, 0x206F], [0x20D0, 0x20EF], [0x302A, 0x302F],
|
||||
[0x3099, 0x309A], [0xA806, 0xA806], [0xA80B, 0xA80B],
|
||||
[0xA825, 0xA826], [0xFB1E, 0xFB1E], [0xFE00, 0xFE0F],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB],
|
||||
[0xFE20, 0xFE23], [0xFEFF, 0xFEFF], [0xFFF9, 0xFFFB]
|
||||
];
|
||||
const COMBINING_HIGH = [
|
||||
[0x10A01, 0x10A03], [0x10A05, 0x10A06], [0x10A0C, 0x10A0F],
|
||||
@@ -121,7 +121,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
}
|
||||
const control = opts.control | 0;
|
||||
let table: number[] | Uint32Array = null;
|
||||
function init_table(): number[] | Uint32Array {
|
||||
function initTable(): number[] | Uint32Array {
|
||||
// lookup table for BMP
|
||||
const CODEPOINTS = 65536; // BMP holds 65536 codepoints
|
||||
const BITWIDTH = 2; // a codepoint can have a width of 0, 1 or 2
|
||||
@@ -161,7 +161,7 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
if (num < 127) {
|
||||
return 1;
|
||||
}
|
||||
let t = table || init_table();
|
||||
const t = table || initTable();
|
||||
if (num < 65536) {
|
||||
return t[num >> 4] >> ((num & 15) << 1) & 3;
|
||||
}
|
||||
|
||||
@@ -5,19 +5,20 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CompositionHelper } from './CompositionHelper';
|
||||
import { ITerminal } from './Types';
|
||||
|
||||
describe('CompositionHelper', () => {
|
||||
let terminal;
|
||||
let compositionHelper;
|
||||
let compositionView;
|
||||
let textarea;
|
||||
let handledText;
|
||||
let terminal: ITerminal;
|
||||
let compositionHelper: CompositionHelper;
|
||||
let compositionView: HTMLElement;
|
||||
let textarea: HTMLTextAreaElement;
|
||||
let handledText: string;
|
||||
|
||||
beforeEach(() => {
|
||||
compositionView = {
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {},
|
||||
remove: () => {}
|
||||
},
|
||||
getBoundingClientRect: () => {
|
||||
return { width: 0 };
|
||||
@@ -27,14 +28,14 @@ describe('CompositionHelper', () => {
|
||||
top: 0
|
||||
},
|
||||
textContent: ''
|
||||
};
|
||||
} as any;
|
||||
textarea = {
|
||||
value: '',
|
||||
style: {
|
||||
left: 0,
|
||||
top: 0
|
||||
}
|
||||
};
|
||||
} as any;
|
||||
terminal = {
|
||||
element: {
|
||||
querySelector: () => {
|
||||
@@ -54,7 +55,7 @@ describe('CompositionHelper', () => {
|
||||
options: {
|
||||
lineHeight: 1
|
||||
}
|
||||
};
|
||||
} as any;
|
||||
handledText = '';
|
||||
compositionHelper = new CompositionHelper(textarea, compositionView, terminal);
|
||||
});
|
||||
@@ -63,7 +64,7 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert simple characters', (done) => {
|
||||
// First character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -71,7 +72,7 @@ describe('CompositionHelper', () => {
|
||||
assert.equal(handledText, 'ㅇ');
|
||||
// Second character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent><CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -87,13 +88,13 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert complex characters', (done) => {
|
||||
// First character '앙'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
textarea.value = '아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
textarea.value = '앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -101,13 +102,13 @@ describe('CompositionHelper', () => {
|
||||
assert.equal(handledText, '앙');
|
||||
// Second character '앙'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = '앙ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
textarea.value = '앙아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
textarea.value = '앙앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -127,19 +128,19 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert complex characters that change with following character', (done) => {
|
||||
// First character '아'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
textarea.value = '아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Start second character '아' in first character
|
||||
compositionHelper.compositionupdate({ data: '앙' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '앙' });
|
||||
textarea.value = '앙';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: '아' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '아' });
|
||||
textarea.value = '아아';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -156,14 +157,14 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-characters compositions', (done) => {
|
||||
// First character 'だ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'd' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
|
||||
textarea.value = 'd';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: 'だ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
|
||||
textarea.value = 'だ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'あ'
|
||||
compositionHelper.compositionupdate({ data: 'だあ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だあ' });
|
||||
textarea.value = 'だあ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -179,18 +180,18 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-character compositions that are converted to other characters with the same length', (done) => {
|
||||
// First character 'だ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'd' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'd' });
|
||||
textarea.value = 'd';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: 'だ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だ' });
|
||||
textarea.value = 'だ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'ー'
|
||||
compositionHelper.compositionupdate({ data: 'だー' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'だー' });
|
||||
textarea.value = 'だー';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Convert to katakana 'ダー'
|
||||
compositionHelper.compositionupdate({ data: 'ダー' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ダー' });
|
||||
textarea.value = 'ダー';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -207,18 +208,18 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert multi-character compositions that are converted to other characters with different lengths', (done) => {
|
||||
// First character 'い'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'い' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'い' });
|
||||
textarea.value = 'い';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Second character 'ま'
|
||||
compositionHelper.compositionupdate({ data: 'いm' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いm' });
|
||||
textarea.value = 'いm';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionupdate({ data: 'いま' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'いま' });
|
||||
textarea.value = 'いま';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
// Convert to kanji '今'
|
||||
compositionHelper.compositionupdate({ data: '今' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: '今' });
|
||||
textarea.value = '今';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
@@ -235,7 +236,7 @@ describe('CompositionHelper', () => {
|
||||
it('Should insert non-composition characters input immediately after composition characters', (done) => {
|
||||
// First character 'ㅇ'
|
||||
compositionHelper.compositionstart();
|
||||
compositionHelper.compositionupdate({ data: 'ㅇ' });
|
||||
compositionHelper.compositionupdate(<CompositionEvent>{ data: 'ㅇ' });
|
||||
textarea.value = 'ㅇ';
|
||||
setTimeout(() => { // wait for any textarea updates
|
||||
compositionHelper.compositionend();
|
||||
|
||||
+64
-65
@@ -20,54 +20,54 @@ export class CompositionHelper {
|
||||
* Whether input composition is currently happening, eg. via a mobile keyboard, speech input or
|
||||
* IME. This variable determines whether the compositionText should be displayed on the UI.
|
||||
*/
|
||||
private isComposing: boolean;
|
||||
private _isComposing: boolean;
|
||||
|
||||
/**
|
||||
* The position within the input textarea's value of the current composition.
|
||||
*/
|
||||
private compositionPosition: IPosition;
|
||||
private _compositionPosition: IPosition;
|
||||
|
||||
/**
|
||||
* Whether a composition is in the process of being sent, setting this to false will cancel any
|
||||
* in-progress composition.
|
||||
*/
|
||||
private isSendingComposition: boolean;
|
||||
private _isSendingComposition: boolean;
|
||||
|
||||
/**
|
||||
* Creates a new CompositionHelper.
|
||||
* @param textarea The textarea that xterm uses for input.
|
||||
* @param compositionView The element to display the in-progress composition in.
|
||||
* @param terminal The Terminal to forward the finished composition to.
|
||||
* @param _textarea The textarea that xterm uses for input.
|
||||
* @param _compositionView The element to display the in-progress composition in.
|
||||
* @param _terminal The Terminal to forward the finished composition to.
|
||||
*/
|
||||
constructor(
|
||||
private textarea: HTMLTextAreaElement,
|
||||
private compositionView: HTMLElement,
|
||||
private terminal: ITerminal
|
||||
private _textarea: HTMLTextAreaElement,
|
||||
private _compositionView: HTMLElement,
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
this.isComposing = false;
|
||||
this.isSendingComposition = false;
|
||||
this.compositionPosition = { start: null, end: null };
|
||||
this._isComposing = false;
|
||||
this._isSendingComposition = false;
|
||||
this._compositionPosition = { start: null, end: null };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionstart event, activating the composition view.
|
||||
*/
|
||||
public compositionstart(): void {
|
||||
this.isComposing = true;
|
||||
this.compositionPosition.start = this.textarea.value.length;
|
||||
this.compositionView.textContent = '';
|
||||
this.compositionView.classList.add('active');
|
||||
this._isComposing = true;
|
||||
this._compositionPosition.start = this._textarea.value.length;
|
||||
this._compositionView.textContent = '';
|
||||
this._compositionView.classList.add('active');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the compositionupdate event, updating the composition view.
|
||||
* @param {CompositionEvent} ev The event.
|
||||
* @param ev The event.
|
||||
*/
|
||||
public compositionupdate(ev: CompositionEvent): void {
|
||||
this.compositionView.textContent = ev.data;
|
||||
this._compositionView.textContent = ev.data;
|
||||
this.updateCompositionElements();
|
||||
setTimeout(() => {
|
||||
this.compositionPosition.end = this.textarea.value.length;
|
||||
this._compositionPosition.end = this._textarea.value.length;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ export class CompositionHelper {
|
||||
* the handler.
|
||||
*/
|
||||
public compositionend(): void {
|
||||
this.finalizeComposition(true);
|
||||
this._finalizeComposition(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,24 +85,23 @@ export class CompositionHelper {
|
||||
* @return Whether the Terminal should continue processing the keydown event.
|
||||
*/
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
if (this.isComposing || this.isSendingComposition) {
|
||||
if (this._isComposing || this._isSendingComposition) {
|
||||
if (ev.keyCode === 229) {
|
||||
// Continue composing if the keyCode is the "composition character"
|
||||
return false;
|
||||
} else if (ev.keyCode === 16 || ev.keyCode === 17 || ev.keyCode === 18) {
|
||||
// Continue composing if the keyCode is a modifier key
|
||||
return false;
|
||||
} else {
|
||||
// Finish composition immediately. This is mainly here for the case where enter is
|
||||
// pressed and the handler needs to be triggered before the command is executed.
|
||||
this.finalizeComposition(false);
|
||||
}
|
||||
// Finish composition immediately. This is mainly here for the case where enter is
|
||||
// pressed and the handler needs to be triggered before the command is executed.
|
||||
this._finalizeComposition(false);
|
||||
}
|
||||
|
||||
if (ev.keyCode === 229) {
|
||||
// If the "composition character" is used but gets to this point it means a non-composition
|
||||
// character (eg. numbers and punctuation) was pressed when the IME was active.
|
||||
this.handleAnyTextareaChanges();
|
||||
this._handleAnyTextareaChanges();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -117,22 +116,22 @@ export class CompositionHelper {
|
||||
* compositionend event is triggered, such as enter, so that the composition is send before
|
||||
* the command is executed.
|
||||
*/
|
||||
private finalizeComposition(waitForPropogation: boolean): void {
|
||||
this.compositionView.classList.remove('active');
|
||||
this.isComposing = false;
|
||||
this.clearTextareaPosition();
|
||||
private _finalizeComposition(waitForPropogation: boolean): void {
|
||||
this._compositionView.classList.remove('active');
|
||||
this._isComposing = false;
|
||||
this._clearTextareaPosition();
|
||||
|
||||
if (!waitForPropogation) {
|
||||
// Cancel any delayed composition send requests and send the input immediately.
|
||||
this.isSendingComposition = false;
|
||||
const input = this.textarea.value.substring(this.compositionPosition.start, this.compositionPosition.end);
|
||||
this.terminal.handler(input);
|
||||
this._isSendingComposition = false;
|
||||
const input = this._textarea.value.substring(this._compositionPosition.start, this._compositionPosition.end);
|
||||
this._terminal.handler(input);
|
||||
} else {
|
||||
// Make a deep copy of the composition position here as a new compositionstart event may
|
||||
// fire before the setTimeout executes.
|
||||
const currentCompositionPosition = {
|
||||
start: this.compositionPosition.start,
|
||||
end: this.compositionPosition.end,
|
||||
start: this._compositionPosition.start,
|
||||
end: this._compositionPosition.end
|
||||
};
|
||||
|
||||
// Since composition* events happen before the changes take place in the textarea on most
|
||||
@@ -143,22 +142,22 @@ export class CompositionHelper {
|
||||
// - The last compositionupdate event's data property does not always accurately describe
|
||||
// the character, a counter example being Korean where an ending consonsant can move to
|
||||
// the following character if the following input is a vowel.
|
||||
this.isSendingComposition = true;
|
||||
this._isSendingComposition = true;
|
||||
setTimeout(() => {
|
||||
// Ensure that the input has not already been sent
|
||||
if (this.isSendingComposition) {
|
||||
this.isSendingComposition = false;
|
||||
if (this._isSendingComposition) {
|
||||
this._isSendingComposition = false;
|
||||
let input;
|
||||
if (this.isComposing) {
|
||||
if (this._isComposing) {
|
||||
// Use the end position to get the string if a new composition has started.
|
||||
input = this.textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end);
|
||||
} else {
|
||||
// Don't use the end position here in order to pick up any characters after the
|
||||
// composition has finished, for example when typing a non-composition character
|
||||
// (eg. 2) after a composition character.
|
||||
input = this.textarea.value.substring(currentCompositionPosition.start);
|
||||
input = this._textarea.value.substring(currentCompositionPosition.start);
|
||||
}
|
||||
this.terminal.handler(input);
|
||||
this._terminal.handler(input);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
@@ -170,15 +169,15 @@ export class CompositionHelper {
|
||||
* character" (229) is triggered, in order to allow non-composition text to be entered when an
|
||||
* IME is active.
|
||||
*/
|
||||
private handleAnyTextareaChanges(): void {
|
||||
const oldValue = this.textarea.value;
|
||||
private _handleAnyTextareaChanges(): void {
|
||||
const oldValue = this._textarea.value;
|
||||
setTimeout(() => {
|
||||
// Ignore if a composition has started since the timeout
|
||||
if (!this.isComposing) {
|
||||
const newValue = this.textarea.value;
|
||||
if (!this._isComposing) {
|
||||
const newValue = this._textarea.value;
|
||||
const diff = newValue.replace(oldValue, '');
|
||||
if (diff.length > 0) {
|
||||
this.terminal.handler(diff);
|
||||
this._terminal.handler(diff);
|
||||
}
|
||||
}
|
||||
}, 0);
|
||||
@@ -191,27 +190,27 @@ export class CompositionHelper {
|
||||
* necessary as the IME events across browsers are not consistently triggered.
|
||||
*/
|
||||
public updateCompositionElements(dontRecurse?: boolean): void {
|
||||
if (!this.isComposing) {
|
||||
if (!this._isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this.terminal.charMeasure.height * this.terminal.options.lineHeight);
|
||||
const cursorTop = this.terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this.terminal.buffer.x * this.terminal.charMeasure.width;
|
||||
if (this._terminal.buffer.isCursorInViewport) {
|
||||
const cellHeight = Math.ceil(this._terminal.charMeasure.height * this._terminal.options.lineHeight);
|
||||
const cursorTop = this._terminal.buffer.y * cellHeight;
|
||||
const cursorLeft = this._terminal.buffer.x * this._terminal.charMeasure.width;
|
||||
|
||||
this.compositionView.style.left = cursorLeft + 'px';
|
||||
this.compositionView.style.top = cursorTop + 'px';
|
||||
this.compositionView.style.height = cellHeight + 'px';
|
||||
this.compositionView.style.lineHeight = cellHeight + 'px';
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
this._compositionView.style.height = cellHeight + 'px';
|
||||
this._compositionView.style.lineHeight = cellHeight + 'px';
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
const compositionViewBounds = this.compositionView.getBoundingClientRect();
|
||||
this.textarea.style.left = cursorLeft + 'px';
|
||||
this.textarea.style.top = cursorTop + 'px';
|
||||
this.textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this.textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this.textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
const compositionViewBounds = this._compositionView.getBoundingClientRect();
|
||||
this._textarea.style.left = cursorLeft + 'px';
|
||||
this._textarea.style.top = cursorTop + 'px';
|
||||
this._textarea.style.width = compositionViewBounds.width + 'px';
|
||||
this._textarea.style.height = compositionViewBounds.height + 'px';
|
||||
this._textarea.style.lineHeight = compositionViewBounds.height + 'px';
|
||||
}
|
||||
|
||||
if (!dontRecurse) {
|
||||
@@ -223,8 +222,8 @@ export class CompositionHelper {
|
||||
* Clears the textarea's position so that the cursor does not blink on IE.
|
||||
* @private
|
||||
*/
|
||||
private clearTextareaPosition(): void {
|
||||
this.textarea.style.left = '';
|
||||
this.textarea.style.top = '';
|
||||
private _clearTextareaPosition(): void {
|
||||
this._textarea.style.left = '';
|
||||
this._textarea.style.top = '';
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,565 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from './Types';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
|
||||
/**
|
||||
* Returns an array filled with numbers between the low and high parameters (right exclusive).
|
||||
* @param low The low number.
|
||||
* @param high The high number.
|
||||
*/
|
||||
function r(low: number, high: number): number[] {
|
||||
let c = high - low;
|
||||
const arr = new Array(c);
|
||||
while (c--) {
|
||||
arr[c] = --high;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transition table for EscapeSequenceParser.
|
||||
* NOTE: data in the underlying table is packed like this:
|
||||
* currentState << 8 | characterCode --> action << 4 | nextState
|
||||
*/
|
||||
export class TransitionTable {
|
||||
public table: Uint8Array | number[];
|
||||
|
||||
constructor(length: number) {
|
||||
this.table = (typeof Uint8Array === 'undefined')
|
||||
? new Array(length)
|
||||
: new Uint8Array(length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a transition to the transition table.
|
||||
* @param code input character code
|
||||
* @param state current parser state
|
||||
* @param action parser action to be done
|
||||
* @param next next parser state
|
||||
*/
|
||||
add(code: number, state: number, action: number | null, next: number | null): void {
|
||||
this.table[state << 8 | code] = ((action | 0) << 4) | ((next === undefined) ? state : next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add transitions for multiple input character codes.
|
||||
* @param codes input character code array
|
||||
* @param state current parser state
|
||||
* @param action parser action to be done
|
||||
* @param next next parser state
|
||||
*/
|
||||
addMany(codes: number[], state: number, action: number | null, next: number | null): void {
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
this.add(codes[i], state, action, next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default definitions for the VT500_TRANSITION_TABLE.
|
||||
*/
|
||||
const PRINTABLES = r(0x20, 0x7f);
|
||||
const EXECUTABLES = r(0x00, 0x18);
|
||||
EXECUTABLES.push(0x19);
|
||||
EXECUTABLES.concat(r(0x1c, 0x20));
|
||||
const DEFAULT_TRANSITION = ParserAction.ERROR << 4 | ParserState.GROUND;
|
||||
|
||||
/**
|
||||
* VT500 compatible transition table.
|
||||
* Taken from https://vt100.net/emu/dec_ansi_parser.
|
||||
*/
|
||||
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
|
||||
const table: TransitionTable = new TransitionTable(4095);
|
||||
|
||||
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
|
||||
let state: any;
|
||||
|
||||
// table with default transition [any] --> DEFAULT_TRANSITION
|
||||
for (state in states) {
|
||||
// NOTE: table lookup is capped at 0xa0 in parse to keep the table small
|
||||
for (let code = 0; code < 160; ++code) {
|
||||
table.add(code, state, ParserAction.ERROR, ParserState.GROUND);
|
||||
}
|
||||
}
|
||||
// printables
|
||||
table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
|
||||
// global anywhere rules
|
||||
for (state in states) {
|
||||
table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
|
||||
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
|
||||
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
|
||||
table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
|
||||
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
|
||||
}
|
||||
// rules for executables and 7f
|
||||
table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);
|
||||
table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);
|
||||
table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);
|
||||
table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);
|
||||
table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);
|
||||
table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);
|
||||
table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
|
||||
// osc
|
||||
table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
|
||||
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
|
||||
// sos/pm/apc does nothing
|
||||
table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
|
||||
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
|
||||
// csi entries
|
||||
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
|
||||
table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
|
||||
table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
|
||||
// esc_intermediate
|
||||
table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
|
||||
// dcs entry
|
||||
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
|
||||
table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
|
||||
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
|
||||
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
|
||||
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
|
||||
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
|
||||
return table;
|
||||
})();
|
||||
|
||||
/**
|
||||
* Dummy DCS handler as default fallback.
|
||||
*/
|
||||
class DcsDummy implements IDcsHandler {
|
||||
hook(collect: string, params: number[], flag: number): void { }
|
||||
put(data: string, start: number, end: number): void { }
|
||||
unhook(): void { }
|
||||
}
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser.
|
||||
* This class implements the ANSI/DEC compatible parser described by
|
||||
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
|
||||
* To implement custom ANSI compliant escape sequences it is not needed to
|
||||
* alter this parser, instead consider registering a custom handler.
|
||||
* For non ANSI compliant sequences change the transition table with
|
||||
* the optional `transitions` contructor argument and
|
||||
* reimplement the `parse` method.
|
||||
* NOTE: The parameter element notation is currently not supported.
|
||||
* TODO: implement error recovery hook via error handler return values
|
||||
*/
|
||||
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
|
||||
public initialState: number;
|
||||
public currentState: number;
|
||||
|
||||
// buffers over several parse calls
|
||||
protected _osc: string;
|
||||
protected _params: number[];
|
||||
protected _collect: string;
|
||||
|
||||
// handler lookup containers
|
||||
protected _printHandler: (data: string, start: number, end: number) => void;
|
||||
protected _executeHandlers: any;
|
||||
protected _csiHandlers: any;
|
||||
protected _escHandlers: any;
|
||||
protected _oscHandlers: any;
|
||||
protected _dcsHandlers: any;
|
||||
protected _activeDcsHandler: IDcsHandler | null;
|
||||
protected _errorHandler: (state: IParsingState) => IParsingState;
|
||||
|
||||
// fallback handlers
|
||||
protected _printHandlerFb: (data: string, start: number, end: number) => void;
|
||||
protected _executeHandlerFb: (code: number) => void;
|
||||
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
|
||||
protected _escHandlerFb: (collect: string, flag: number) => void;
|
||||
protected _oscHandlerFb: (identifier: number, data: string) => void;
|
||||
protected _dcsHandlerFb: IDcsHandler;
|
||||
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
|
||||
|
||||
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
|
||||
super();
|
||||
|
||||
this.initialState = ParserState.GROUND;
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._params = [0];
|
||||
this._collect = '';
|
||||
|
||||
// set default fallback handlers and handler lookup containers
|
||||
this._printHandlerFb = (data, start, end): void => { };
|
||||
this._executeHandlerFb = (code: number): void => { };
|
||||
this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { };
|
||||
this._escHandlerFb = (collect: string, flag: number): void => { };
|
||||
this._oscHandlerFb = (identifier: number, data: string): void => { };
|
||||
this._dcsHandlerFb = new DcsDummy();
|
||||
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
|
||||
this._printHandler = this._printHandlerFb;
|
||||
this._executeHandlers = Object.create(null);
|
||||
this._csiHandlers = Object.create(null);
|
||||
this._escHandlers = Object.create(null);
|
||||
this._oscHandlers = Object.create(null);
|
||||
this._dcsHandlers = Object.create(null);
|
||||
this._activeDcsHandler = null;
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._printHandlerFb = null;
|
||||
this._executeHandlerFb = null;
|
||||
this._csiHandlerFb = null;
|
||||
this._escHandlerFb = null;
|
||||
this._oscHandlerFb = null;
|
||||
this._dcsHandlerFb = null;
|
||||
this._errorHandlerFb = null;
|
||||
this._printHandler = null;
|
||||
this._executeHandlers = null;
|
||||
this._csiHandlers = null;
|
||||
this._escHandlers = null;
|
||||
this._oscHandlers = null;
|
||||
this._dcsHandlers = null;
|
||||
this._activeDcsHandler = null;
|
||||
this._errorHandler = null;
|
||||
}
|
||||
|
||||
setPrintHandler(callback: (data: string, start: number, end: number) => void): void {
|
||||
this._printHandler = callback;
|
||||
}
|
||||
clearPrintHandler(): void {
|
||||
this._printHandler = this._printHandlerFb;
|
||||
}
|
||||
|
||||
setExecuteHandler(flag: string, callback: () => void): void {
|
||||
this._executeHandlers[flag.charCodeAt(0)] = callback;
|
||||
}
|
||||
clearExecuteHandler(flag: string): void {
|
||||
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
|
||||
}
|
||||
setExecuteHandlerFallback(callback: (code: number) => void): void {
|
||||
this._executeHandlerFb = callback;
|
||||
}
|
||||
|
||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
|
||||
this._csiHandlers[flag.charCodeAt(0)] = callback;
|
||||
}
|
||||
clearCsiHandler(flag: string): void {
|
||||
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
|
||||
}
|
||||
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void {
|
||||
this._csiHandlerFb = callback;
|
||||
}
|
||||
|
||||
setEscHandler(collectAndFlag: string, callback: () => void): void {
|
||||
this._escHandlers[collectAndFlag] = callback;
|
||||
}
|
||||
clearEscHandler(collectAndFlag: string): void {
|
||||
if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag];
|
||||
}
|
||||
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void {
|
||||
this._escHandlerFb = callback;
|
||||
}
|
||||
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void {
|
||||
this._oscHandlers[ident] = callback;
|
||||
}
|
||||
clearOscHandler(ident: number): void {
|
||||
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
|
||||
}
|
||||
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
|
||||
this._oscHandlerFb = callback;
|
||||
}
|
||||
|
||||
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
|
||||
this._dcsHandlers[collectAndFlag] = handler;
|
||||
}
|
||||
clearDcsHandler(collectAndFlag: string): void {
|
||||
if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag];
|
||||
}
|
||||
setDcsHandlerFallback(handler: IDcsHandler): void {
|
||||
this._dcsHandlerFb = handler;
|
||||
}
|
||||
|
||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
|
||||
this._errorHandler = callback;
|
||||
}
|
||||
clearErrorHandler(): void {
|
||||
this._errorHandler = this._errorHandlerFb;
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.currentState = this.initialState;
|
||||
this._osc = '';
|
||||
this._params = [0];
|
||||
this._collect = '';
|
||||
this._activeDcsHandler = null;
|
||||
}
|
||||
|
||||
parse(data: string): void {
|
||||
let code = 0;
|
||||
let transition = 0;
|
||||
let error = false;
|
||||
let currentState = this.currentState;
|
||||
let print = -1;
|
||||
let dcs = -1;
|
||||
let osc = this._osc;
|
||||
let collect = this._collect;
|
||||
let params = this._params;
|
||||
const table: Uint8Array | number[] = this.TRANSITIONS.table;
|
||||
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
|
||||
let callback: Function | null = null;
|
||||
|
||||
// process input string
|
||||
const l = data.length;
|
||||
for (let i = 0; i < l; ++i) {
|
||||
code = data.charCodeAt(i);
|
||||
|
||||
// shortcut for most chars (print action)
|
||||
if (currentState === ParserState.GROUND && code > 0x1f && code < 0x80) {
|
||||
print = (~print) ? print : i;
|
||||
do i++;
|
||||
while (i < l && data.charCodeAt(i) > 0x1f && data.charCodeAt(i) < 0x80);
|
||||
i--;
|
||||
continue;
|
||||
}
|
||||
|
||||
// shortcut for CSI params
|
||||
if (currentState === ParserState.CSI_PARAM && (code > 0x2f && code < 0x39)) {
|
||||
params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
|
||||
continue;
|
||||
}
|
||||
|
||||
// normal transition & action lookup
|
||||
transition = (code < 0xa0) ? (table[currentState << 8 | code]) : DEFAULT_TRANSITION;
|
||||
switch (transition >> 4) {
|
||||
case ParserAction.PRINT:
|
||||
print = (~print) ? print : i;
|
||||
break;
|
||||
case ParserAction.EXECUTE:
|
||||
if (~print) {
|
||||
this._printHandler(data, print, i);
|
||||
print = -1;
|
||||
}
|
||||
callback = this._executeHandlers[code];
|
||||
if (callback) callback();
|
||||
else this._executeHandlerFb(code);
|
||||
break;
|
||||
case ParserAction.IGNORE:
|
||||
// handle leftover print or dcs chars
|
||||
if (~print) {
|
||||
this._printHandler(data, print, i);
|
||||
print = -1;
|
||||
} else if (~dcs) {
|
||||
dcsHandler.put(data, dcs, i);
|
||||
dcs = -1;
|
||||
}
|
||||
break;
|
||||
case ParserAction.ERROR:
|
||||
// chars higher than 0x9f are handled by this action
|
||||
// to keep the transition table small
|
||||
if (code > 0x9f) {
|
||||
switch (currentState) {
|
||||
case ParserState.GROUND:
|
||||
print = (~print) ? print : i;
|
||||
break;
|
||||
case ParserState.OSC_STRING:
|
||||
osc += String.fromCharCode(code);
|
||||
transition |= ParserState.OSC_STRING;
|
||||
break;
|
||||
case ParserState.CSI_IGNORE:
|
||||
transition |= ParserState.CSI_IGNORE;
|
||||
break;
|
||||
case ParserState.DCS_IGNORE:
|
||||
transition |= ParserState.DCS_IGNORE;
|
||||
break;
|
||||
case ParserState.DCS_PASSTHROUGH:
|
||||
dcs = (~dcs) ? dcs : i;
|
||||
transition |= ParserState.DCS_PASSTHROUGH;
|
||||
break;
|
||||
default:
|
||||
error = true;
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
// if we end up here a real error happened
|
||||
if (error) {
|
||||
const inject: IParsingState = this._errorHandler(
|
||||
{
|
||||
position: i,
|
||||
code,
|
||||
currentState,
|
||||
print,
|
||||
dcs,
|
||||
osc,
|
||||
collect,
|
||||
params,
|
||||
abort: false
|
||||
});
|
||||
if (inject.abort) return;
|
||||
// TODO: inject return values
|
||||
error = false;
|
||||
}
|
||||
break;
|
||||
case ParserAction.CSI_DISPATCH:
|
||||
callback = this._csiHandlers[code];
|
||||
if (callback) callback(params, collect);
|
||||
else this._csiHandlerFb(collect, params, code);
|
||||
break;
|
||||
case ParserAction.PARAM:
|
||||
if (code === 0x3b) params.push(0);
|
||||
else params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
|
||||
break;
|
||||
case ParserAction.COLLECT:
|
||||
collect += String.fromCharCode(code);
|
||||
break;
|
||||
case ParserAction.ESC_DISPATCH:
|
||||
callback = this._escHandlers[collect + String.fromCharCode(code)];
|
||||
if (callback) callback(collect, code);
|
||||
else this._escHandlerFb(collect, code);
|
||||
break;
|
||||
case ParserAction.CLEAR:
|
||||
if (~print) {
|
||||
this._printHandler(data, print, i);
|
||||
print = -1;
|
||||
}
|
||||
osc = '';
|
||||
params = [0];
|
||||
collect = '';
|
||||
dcs = -1;
|
||||
break;
|
||||
case ParserAction.DCS_HOOK:
|
||||
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
|
||||
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
|
||||
dcsHandler.hook(collect, params, code);
|
||||
break;
|
||||
case ParserAction.DCS_PUT:
|
||||
dcs = (~dcs) ? dcs : i;
|
||||
break;
|
||||
case ParserAction.DCS_UNHOOK:
|
||||
if (dcsHandler) {
|
||||
if (~dcs) dcsHandler.put(data, dcs, i);
|
||||
dcsHandler.unhook();
|
||||
dcsHandler = null;
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
params = [0];
|
||||
collect = '';
|
||||
dcs = -1;
|
||||
break;
|
||||
case ParserAction.OSC_START:
|
||||
if (~print) {
|
||||
this._printHandler(data, print, i);
|
||||
print = -1;
|
||||
}
|
||||
osc = '';
|
||||
break;
|
||||
case ParserAction.OSC_PUT:
|
||||
osc += data.charAt(i);
|
||||
break;
|
||||
case ParserAction.OSC_END:
|
||||
if (osc && code !== 0x18 && code !== 0x1a) {
|
||||
// NOTE: OSC subparsing is not part of the original parser
|
||||
// we do basic identifier parsing here to offer a jump table for OSC as well
|
||||
const idx = osc.indexOf(';');
|
||||
if (idx === -1) {
|
||||
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
|
||||
} else {
|
||||
// Note: NaN is not handled here
|
||||
// either catch it with the fallback handler
|
||||
// or with an explicit NaN OSC handler
|
||||
const identifier = parseInt(osc.substring(0, idx));
|
||||
const content = osc.substring(idx + 1);
|
||||
callback = this._oscHandlers[identifier];
|
||||
if (callback) callback(content);
|
||||
else this._oscHandlerFb(identifier, content);
|
||||
}
|
||||
}
|
||||
if (code === 0x1b) transition |= ParserState.ESCAPE;
|
||||
osc = '';
|
||||
params = [0];
|
||||
collect = '';
|
||||
dcs = -1;
|
||||
break;
|
||||
}
|
||||
currentState = transition & 15;
|
||||
}
|
||||
|
||||
// push leftover pushable buffers to terminal
|
||||
if (currentState === ParserState.GROUND && ~print) {
|
||||
this._printHandler(data, print, data.length);
|
||||
} else if (currentState === ParserState.DCS_PASSTHROUGH && ~dcs && dcsHandler) {
|
||||
dcsHandler.put(data, dcs, data.length);
|
||||
}
|
||||
|
||||
// save non pushable buffers
|
||||
this._osc = osc;
|
||||
this._collect = collect;
|
||||
this._params = params;
|
||||
|
||||
// save active dcs handler reference
|
||||
this._activeDcsHandler = dcsHandler;
|
||||
|
||||
// save state
|
||||
this.currentState = currentState;
|
||||
}
|
||||
}
|
||||
+33
-9
@@ -3,28 +3,51 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEventEmitter } from 'xterm';
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
|
||||
export class EventEmitter implements IEventEmitter {
|
||||
private _events: {[type: string]: ((...args: any[]) => void)[]};
|
||||
export class EventEmitter extends Disposable implements IEventEmitter, IDisposable {
|
||||
private _events: {[type: string]: XtermListener[]};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
// Restore the previous events if available, this will happen if the
|
||||
// constructor is called multiple times on the same object (terminal reset).
|
||||
this._events = this._events || {};
|
||||
}
|
||||
|
||||
public on(type: string, listener: ((...args: any[]) => void)): void {
|
||||
public on(type: string, listener: XtermListener): void {
|
||||
this._events[type] = this._events[type] || [];
|
||||
this._events[type].push(listener);
|
||||
}
|
||||
|
||||
public off(type: string, listener: ((...args: any[]) => void)): void {
|
||||
/**
|
||||
* Adds a disposabe listener to the EventEmitter, returning the disposable.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
*/
|
||||
public addDisposableListener(type: string, handler: XtermListener): IDisposable {
|
||||
// TODO: Rename addDisposableEventListener to more easily disambiguate from Dom listener
|
||||
this.on(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
this.off(type, handler);
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public off(type: string, listener: XtermListener): void {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = this._events[type];
|
||||
const obj = this._events[type];
|
||||
let i = obj.length;
|
||||
|
||||
while (i--) {
|
||||
@@ -45,17 +68,18 @@ export class EventEmitter implements IEventEmitter {
|
||||
if (!this._events[type]) {
|
||||
return;
|
||||
}
|
||||
let obj = this._events[type];
|
||||
const obj = this._events[type];
|
||||
for (let i = 0; i < obj.length; i++) {
|
||||
obj[i].apply(this, args);
|
||||
}
|
||||
}
|
||||
|
||||
public listeners(type: string): ((...args: any[]) => void)[] {
|
||||
public listeners(type: string): XtermListener[] {
|
||||
return this._events[type] || [];
|
||||
}
|
||||
|
||||
protected destroy(): void {
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._events = {};
|
||||
}
|
||||
}
|
||||
|
||||
+413
-17
@@ -3,80 +3,476 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { assert, expect } from 'chai';
|
||||
import { InputHandler } from './InputHandler';
|
||||
import { MockInputHandlingTerminal } from './utils/TestUtils.test';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { Terminal } from './Terminal';
|
||||
import { IBufferLine } from './Types';
|
||||
|
||||
|
||||
// TODO: This and the sections related to this object in associated tests can be
|
||||
// removed safely after InputHandler refactors are finished
|
||||
class OldInputHandler extends InputHandler {
|
||||
public eraseInLine(params: number[]): void {
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
break;
|
||||
case 2:
|
||||
this.eraseLine(this._terminal.buffer.y);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public eraseInDisplay(params: number[]): void {
|
||||
let j;
|
||||
switch (params[0]) {
|
||||
case 0:
|
||||
this.eraseRight(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y + 1;
|
||||
for (; j < this._terminal.rows; j++) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.eraseLeft(this._terminal.buffer.x, this._terminal.buffer.y);
|
||||
j = this._terminal.buffer.y;
|
||||
while (j--) {
|
||||
this.eraseLine(j);
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
j = this._terminal.rows;
|
||||
while (j--) this.eraseLine(j);
|
||||
break;
|
||||
case 3:
|
||||
// Clear scrollback (everything not in viewport)
|
||||
const scrollBackSize = this._terminal.buffer.lines.length - this._terminal.rows;
|
||||
if (scrollBackSize > 0) {
|
||||
this._terminal.buffer.lines.trimStart(scrollBackSize);
|
||||
this._terminal.buffer.ybase = Math.max(this._terminal.buffer.ybase - scrollBackSize, 0);
|
||||
this._terminal.buffer.ydisp = Math.max(this._terminal.buffer.ydisp - scrollBackSize, 0);
|
||||
// Force a scroll event to refresh viewport
|
||||
this._terminal.emit('scroll', 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the end of the line (right).
|
||||
* @param x The column from which to start erasing to the end of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseRight(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(x, this._terminal.cols, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase in the identified line everything from "x" to the start of the line (left).
|
||||
* @param x The column from which to start erasing to the start of the line.
|
||||
* @param y The line in which to operate.
|
||||
*/
|
||||
public eraseLeft(x: number, y: number): void {
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ybase + y);
|
||||
if (!line) {
|
||||
return;
|
||||
}
|
||||
line.replaceCells(0, x + 1, [this._terminal.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
this._terminal.updateRange(y);
|
||||
}
|
||||
|
||||
/**
|
||||
* Erase all content in the given line
|
||||
* @param y The line to erase all of its contents.
|
||||
*/
|
||||
public eraseLine(y: number): void {
|
||||
this.eraseRight(0, y);
|
||||
}
|
||||
}
|
||||
|
||||
describe('InputHandler', () => {
|
||||
describe('save and restore cursor', () => {
|
||||
let terminal = new MockInputHandlingTerminal();
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
terminal.buffer.x = 1;
|
||||
terminal.buffer.y = 2;
|
||||
let inputHandler = new InputHandler(terminal);
|
||||
terminal.curAttr = 3;
|
||||
const inputHandler = new InputHandler(terminal);
|
||||
// Save cursor position
|
||||
inputHandler.saveCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
// Change cursor position
|
||||
terminal.buffer.x = 10;
|
||||
terminal.buffer.y = 20;
|
||||
terminal.curAttr = 30;
|
||||
// Restore cursor position
|
||||
inputHandler.restoreCursor([]);
|
||||
assert.equal(terminal.buffer.x, 1);
|
||||
assert.equal(terminal.buffer.y, 2);
|
||||
assert.equal(terminal.curAttr, 3);
|
||||
});
|
||||
describe('setCursorStyle', () => {
|
||||
it('should call Terminal.setOption with correct params', () => {
|
||||
let terminal = new MockInputHandlingTerminal();
|
||||
let inputHandler = new InputHandler(terminal);
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
const inputHandler = new InputHandler(terminal);
|
||||
const collect = ' ';
|
||||
|
||||
inputHandler.setCursorStyle([0]);
|
||||
inputHandler.setCursorStyle([0], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'block');
|
||||
assert.equal(terminal.options['cursorBlink'], true);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([1]);
|
||||
inputHandler.setCursorStyle([1], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'block');
|
||||
assert.equal(terminal.options['cursorBlink'], true);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([2]);
|
||||
inputHandler.setCursorStyle([2], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'block');
|
||||
assert.equal(terminal.options['cursorBlink'], false);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([3]);
|
||||
inputHandler.setCursorStyle([3], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'underline');
|
||||
assert.equal(terminal.options['cursorBlink'], true);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([4]);
|
||||
inputHandler.setCursorStyle([4], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'underline');
|
||||
assert.equal(terminal.options['cursorBlink'], false);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([5]);
|
||||
inputHandler.setCursorStyle([5], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'bar');
|
||||
assert.equal(terminal.options['cursorBlink'], true);
|
||||
|
||||
terminal.options = {};
|
||||
inputHandler.setCursorStyle([6]);
|
||||
inputHandler.setCursorStyle([6], collect);
|
||||
assert.equal(terminal.options['cursorStyle'], 'bar');
|
||||
assert.equal(terminal.options['cursorBlink'], false);
|
||||
});
|
||||
});
|
||||
describe('setMode', () => {
|
||||
it('should toggle Terminal.bracketedPasteMode', () => {
|
||||
let terminal = new MockInputHandlingTerminal();
|
||||
terminal.prefix = '?';
|
||||
const terminal = new MockInputHandlingTerminal();
|
||||
const collect = '?';
|
||||
terminal.bracketedPasteMode = false;
|
||||
let inputHandler = new InputHandler(terminal);
|
||||
const inputHandler = new InputHandler(terminal);
|
||||
// Set bracketed paste mode
|
||||
inputHandler.setMode([2004]);
|
||||
inputHandler.setMode([2004], collect);
|
||||
assert.equal(terminal.bracketedPasteMode, true);
|
||||
// Reset bracketed paste mode
|
||||
inputHandler.resetMode([2004]);
|
||||
inputHandler.resetMode([2004], collect);
|
||||
assert.equal(terminal.bracketedPasteMode, false);
|
||||
});
|
||||
});
|
||||
describe('regression tests', function(): void {
|
||||
type CharData = [number, string, number, number];
|
||||
|
||||
function lineContent(line: IBufferLine): string {
|
||||
let content = '';
|
||||
for (let i = 0; i < line.length; ++i) content += line.get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
return content;
|
||||
}
|
||||
|
||||
function termContent(term: Terminal): string[] {
|
||||
const result = [];
|
||||
for (let i = 0; i < term.rows; ++i) result.push(lineContent(term.buffer.lines.get(i)));
|
||||
return result;
|
||||
}
|
||||
|
||||
it('insertChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
|
||||
// old variant of the method
|
||||
function insertChars(params: number[]): void {
|
||||
let param = params[0];
|
||||
if (param < 1) param = 1;
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = term.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
let j = buffer.x;
|
||||
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||
while (param-- && j < term.cols) {
|
||||
buffer.lines.get(row).splice(j++, 0, ch);
|
||||
buffer.lines.get(row).pop();
|
||||
}
|
||||
}
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// insert one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
insertChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([0]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456789');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
insertChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([1]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 12345678');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
insertChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([2]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' 123456');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
insertChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.insertChars([10]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
});
|
||||
it('deleteChars', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
|
||||
// old variant of the method
|
||||
function deleteChars(params: number[]): void {
|
||||
let param: number = params[0];
|
||||
if (param < 1) {
|
||||
param = 1;
|
||||
}
|
||||
|
||||
// make buffer local for faster access
|
||||
const buffer = term.buffer;
|
||||
|
||||
const row = buffer.y + buffer.ybase;
|
||||
const ch: CharData = [term.eraseAttr(), NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]; // xterm
|
||||
while (param--) {
|
||||
buffer.lines.get(row).splice(buffer.x, 1);
|
||||
buffer.lines.get(row).push(ch);
|
||||
}
|
||||
term.updateRange(buffer.y);
|
||||
}
|
||||
|
||||
// insert some data in first and second line
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
inputHandler.parse(Array(term.cols - 9).join('a'));
|
||||
inputHandler.parse('1234567890');
|
||||
const line1: IBufferLine = term.buffer.lines.get(0); // line for old variant
|
||||
const line2: IBufferLine = term.buffer.lines.get(1); // line for new variant
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '1234567890');
|
||||
|
||||
// delete one char from params = [0]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
deleteChars([0]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([0]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '234567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert one char from params = [1]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
deleteChars([1]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([1]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '34567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert two chars from params = [2]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
deleteChars([2]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([2]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + '567890 ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
|
||||
// insert 10 chars from params = [10]
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
deleteChars([10]);
|
||||
expect(lineContent(line1)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.deleteChars([10]);
|
||||
expect(lineContent(line2)).equals(Array(term.cols - 9).join('a') + ' ');
|
||||
expect(lineContent(line2)).equals(lineContent(line1));
|
||||
});
|
||||
it('eraseInLine', function(): void {
|
||||
const term = new Terminal();
|
||||
const inputHandler = new InputHandler(term);
|
||||
const oldInputHandler = new OldInputHandler(term);
|
||||
|
||||
// fill 6 lines to test 3 different states
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
inputHandler.parse(Array(term.cols + 1).join('a'));
|
||||
|
||||
// params[0] - right erase
|
||||
term.buffer.y = 0;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(0))).equals(Array(71).join('a') + ' ');
|
||||
term.buffer.y = 1;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([0]);
|
||||
expect(lineContent(term.buffer.lines.get(1))).equals(Array(71).join('a') + ' ');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 2;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(2))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
term.buffer.y = 3;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([1]);
|
||||
expect(lineContent(term.buffer.lines.get(3))).equals(Array(71).join(' ') + ' aaaaaaaaa');
|
||||
|
||||
// params[1] - left erase
|
||||
term.buffer.y = 4;
|
||||
term.buffer.x = 70;
|
||||
oldInputHandler.eraseInLine([2]);
|
||||
expect(lineContent(term.buffer.lines.get(4))).equals(Array(term.cols + 1).join(' '));
|
||||
term.buffer.y = 5;
|
||||
term.buffer.x = 70;
|
||||
inputHandler.eraseInLine([2]);
|
||||
expect(lineContent(term.buffer.lines.get(5))).equals(Array(term.cols + 1).join(' '));
|
||||
|
||||
});
|
||||
it('eraseInDisplay', function(): void {
|
||||
const termOld = new Terminal();
|
||||
const inputHandlerOld = new OldInputHandler(termOld);
|
||||
const termNew = new Terminal();
|
||||
const inputHandlerNew = new InputHandler(termNew);
|
||||
|
||||
// fill display with a's
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
const data = [];
|
||||
for (let i = 0; i < termOld.rows; ++i) data.push(Array(termOld.cols + 1).join('a'));
|
||||
expect(termContent(termOld)).eql(data);
|
||||
expect(termContent(termOld)).eql(termContent(termNew));
|
||||
|
||||
// params [0] - right and below erase
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([0]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([0]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
|
||||
// reset
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
|
||||
// params [1] - left and above
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([1]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([1]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
|
||||
// reset
|
||||
termOld.buffer.y = 0;
|
||||
termOld.buffer.x = 0;
|
||||
termNew.buffer.y = 0;
|
||||
termNew.buffer.x = 0;
|
||||
for (let i = 0; i < termOld.rows; ++i) inputHandlerOld.parse(Array(termOld.cols + 1).join('a'));
|
||||
for (let i = 0; i < termNew.rows; ++i) inputHandlerNew.parse(Array(termOld.cols + 1).join('a'));
|
||||
|
||||
// params [2] - whole screen
|
||||
termOld.buffer.y = 5;
|
||||
termOld.buffer.x = 40;
|
||||
inputHandlerOld.eraseInDisplay([2]);
|
||||
termNew.buffer.y = 5;
|
||||
termNew.buffer.x = 40;
|
||||
inputHandlerNew.eraseInDisplay([2]);
|
||||
expect(termContent(termNew)).eql(termContent(termOld));
|
||||
});
|
||||
});
|
||||
it('convertEol setting', function(): void {
|
||||
// not converting
|
||||
let s = '';
|
||||
const termNotConverting = new Terminal({cols: 15, rows: 10});
|
||||
(termNotConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termNotConverting.cols; ++i) {
|
||||
s += termNotConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals(' World ');
|
||||
|
||||
// converting
|
||||
s = '';
|
||||
const termConverting = new Terminal({cols: 15, rows: 10, convertEol: true});
|
||||
(termConverting as any)._inputHandler.parse('Hello\nWorld');
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(0).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('Hello ');
|
||||
s = '';
|
||||
for (let i = 0; i < termConverting.cols; ++i) {
|
||||
s += termConverting.buffer.lines.get(1).get(i)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
expect(s).equals('World ');
|
||||
});
|
||||
});
|
||||
|
||||
+750
-214
File diff suppressed because it is too large
Load Diff
+59
-37
@@ -4,16 +4,17 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Types';
|
||||
import { ILinkMatcher, LineData, ITerminal, ILinkifier, IBuffer, IBufferAccessor, IElementAccessor } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer } from './utils/TestUtils.test';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(_terminal: IBufferAccessor & IElementAccessor) {
|
||||
super(_terminal);
|
||||
Linkifier.TIME_BEFORE_LINKIFY = 0;
|
||||
constructor(terminal: ITerminal) {
|
||||
super(terminal);
|
||||
(<any>Linkifier).TIME_BEFORE_LINKIFY = 0;
|
||||
}
|
||||
|
||||
public get linkMatchers(): ILinkMatcher[] { return this._linkMatchers; }
|
||||
@@ -21,6 +22,8 @@ class TestLinkifier extends Linkifier {
|
||||
}
|
||||
|
||||
class TestMouseZoneManager implements IMouseZoneManager {
|
||||
dispose(): void {
|
||||
}
|
||||
public clears: number = 0;
|
||||
public zones: IMouseZone[] = [];
|
||||
add(zone: IMouseZone): void {
|
||||
@@ -32,23 +35,22 @@ class TestMouseZoneManager implements IMouseZoneManager {
|
||||
}
|
||||
|
||||
describe('Linkifier', () => {
|
||||
let terminal: IBufferAccessor & IElementAccessor;
|
||||
let terminal: ITerminal;
|
||||
let linkifier: TestLinkifier;
|
||||
let mouseZoneManager: TestMouseZoneManager;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = {
|
||||
buffer: new MockBuffer(),
|
||||
element: <HTMLElement>{}
|
||||
};
|
||||
terminal.buffer.lines = new CircularList<LineData>(20);
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 100;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<IBufferLine>(20));
|
||||
terminal.buffer.ydisp = 0;
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
});
|
||||
|
||||
function stringToRow(text: string): LineData {
|
||||
let result: LineData = [];
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine();
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
@@ -59,17 +61,6 @@ describe('Linkifier', () => {
|
||||
terminal.buffer.lines.push(stringToRow(text));
|
||||
}
|
||||
|
||||
function assertLinkifiesEntireRow(uri: string, done: MochaDone): void {
|
||||
addRow(uri);
|
||||
linkifier.linkifyRows();
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, uri.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y, terminal.buffer.lines.length);
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
@@ -80,7 +71,25 @@ describe('Linkifier', () => {
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y, terminal.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y1, terminal.buffer.lines.length);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, terminal.buffer.lines.length);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
assert.equal(mouseZoneManager.zones.length, links.length);
|
||||
links.forEach((l, i) => {
|
||||
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
|
||||
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
|
||||
});
|
||||
done();
|
||||
}, 0);
|
||||
@@ -101,12 +110,6 @@ describe('Linkifier', () => {
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
});
|
||||
|
||||
describe('http links', () => {
|
||||
it('should allow ~ character in URI path', (done) => {
|
||||
assertLinkifiesEntireRow('http://foo.com/a~b#c~d?e~f', done);
|
||||
});
|
||||
});
|
||||
|
||||
describe('link matcher', () => {
|
||||
it('should match a single link', done => {
|
||||
assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done);
|
||||
@@ -135,6 +138,24 @@ describe('Linkifier', () => {
|
||||
// character (U+1F537) which caused the path to be duplicated. See #642.
|
||||
assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done);
|
||||
});
|
||||
describe('multi-line links', () => {
|
||||
it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('should match links that start and end on line 2/2 of a wrapped line', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done);
|
||||
});
|
||||
it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => {
|
||||
terminal.cols = 4;
|
||||
assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validationCallback', () => {
|
||||
@@ -147,7 +168,8 @@ describe('Linkifier', () => {
|
||||
assert.equal(mouseZoneManager.zones.length, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].x2, 5);
|
||||
assert.equal(mouseZoneManager.zones[0].y, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y1, 1);
|
||||
assert.equal(mouseZoneManager.zones[0].y2, 1);
|
||||
// Fires done()
|
||||
mouseZoneManager.zones[0].clickCallback(<any>{});
|
||||
}
|
||||
@@ -200,19 +222,19 @@ describe('Linkifier', () => {
|
||||
it('should order the list from highest priority to lowest #1', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, 0, bId]);
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
|
||||
});
|
||||
|
||||
it('should order the list from highest priority to lowest #2', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, 0, aId]);
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]);
|
||||
});
|
||||
|
||||
it('should order items of equal priority in the order they are added', () => {
|
||||
const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 });
|
||||
const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 });
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [0, aId, bId]);
|
||||
assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+75
-71
@@ -3,34 +3,11 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkMatcherValidationCallback, LineData, LinkHoverEventTypes, ILinkMatcherOptions, ITerminal, IBufferAccessor, ILinkifier, IElementAccessor } from './Types';
|
||||
import { MouseZone } from './input/MouseZoneManager';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferLine } from './Types';
|
||||
import { MouseZone } from './ui/MouseZoneManager';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,6})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
|
||||
/**
|
||||
* The ID of the built in http(s) link matcher.
|
||||
*/
|
||||
const HYPERTEXT_LINK_MATCHER_ID = 0;
|
||||
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
@@ -41,24 +18,23 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* the costly operation of searching every row multiple times, potentially a
|
||||
* huge amount of times.
|
||||
*/
|
||||
protected static TIME_BEFORE_LINKIFY = 200;
|
||||
protected static readonly TIME_BEFORE_LINKIFY = 200;
|
||||
|
||||
protected _linkMatchers: ILinkMatcher[] = [];
|
||||
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = HYPERTEXT_LINK_MATCHER_ID;
|
||||
private _rowsToLinkify: {start: number, end: number};
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
|
||||
constructor(
|
||||
protected _terminal: IBufferAccessor & IElementAccessor
|
||||
protected _terminal: ITerminal
|
||||
) {
|
||||
super();
|
||||
this._rowsToLinkify = {
|
||||
start: null,
|
||||
end: null
|
||||
};
|
||||
this.registerLinkMatcher(strictUrlRegex, null, { matchIndex: 1 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,23 +87,6 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
this._rowsToLinkify.end = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a handler for hypertext links, overriding default <a> behavior for
|
||||
* tandard http(s) links.
|
||||
* @param handler The handler to use, this can be cleared with null.
|
||||
*/
|
||||
public setHypertextLinkHandler(handler: LinkMatcherHandler): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].handler = handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a validation callback for hypertext links.
|
||||
* @param callback The callback to use, this can be cleared with null.
|
||||
*/
|
||||
public setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void {
|
||||
this._linkMatchers[HYPERTEXT_LINK_MATCHER_ID].validationCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a link matcher, allowing custom link patterns to be matched and
|
||||
* handled.
|
||||
@@ -139,7 +98,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @return The ID of the new matcher, this can be used to deregister.
|
||||
*/
|
||||
public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number {
|
||||
if (this._nextLinkMatcherId !== HYPERTEXT_LINK_MATCHER_ID && !handler) {
|
||||
if (!handler) {
|
||||
throw new Error('handler must be defined');
|
||||
}
|
||||
const matcher: ILinkMatcher = {
|
||||
@@ -185,8 +144,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @return Whether a link matcher was found and deregistered.
|
||||
*/
|
||||
public deregisterLinkMatcher(matcherId: number): boolean {
|
||||
// ID 0 is the hypertext link matcher which cannot be deregistered
|
||||
for (let i = 1; i < this._linkMatchers.length; i++) {
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
if (this._linkMatchers[i].id === matcherId) {
|
||||
this._linkMatchers.splice(i, 1);
|
||||
return true;
|
||||
@@ -200,11 +158,40 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param rowIndex The index of the row to linkify.
|
||||
*/
|
||||
private _linkifyRow(rowIndex: number): void {
|
||||
const absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
// Ensure the row exists
|
||||
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
const text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
||||
|
||||
if (this._terminal.buffer.lines.get(absoluteRowIndex).isWrapped) {
|
||||
// Only attempt to linkify rows that start in the viewport
|
||||
if (rowIndex !== 0) {
|
||||
return;
|
||||
}
|
||||
// If the first row is wrapped, backtrack to find the origin row and linkify that
|
||||
let line: IBufferLine;
|
||||
|
||||
do {
|
||||
rowIndex--;
|
||||
absoluteRowIndex--;
|
||||
line = this._terminal.buffer.lines.get(absoluteRowIndex);
|
||||
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
|
||||
} while (line.isWrapped);
|
||||
}
|
||||
|
||||
// Construct full unwrapped line text
|
||||
let text = this._terminal.buffer.translateBufferLineToString(absoluteRowIndex, false);
|
||||
let currentIndex = absoluteRowIndex + 1;
|
||||
while (currentIndex < this._terminal.buffer.lines.length &&
|
||||
this._terminal.buffer.lines.get(currentIndex).isWrapped) {
|
||||
text += this._terminal.buffer.translateBufferLineToString(currentIndex++, false);
|
||||
}
|
||||
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
this._doLinkifyRow(rowIndex, text, this._linkMatchers[i]);
|
||||
}
|
||||
@@ -220,20 +207,22 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @return The link element(s) that were added.
|
||||
*/
|
||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
|
||||
// Iterate over nodes as we want to consider text nodes
|
||||
let result = [];
|
||||
const isHttpLinkMatcher = matcher.id === HYPERTEXT_LINK_MATCHER_ID;
|
||||
|
||||
// Find the first match
|
||||
let match = text.match(matcher.regex);
|
||||
const match = text.match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
return;
|
||||
}
|
||||
let uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
const index = text.indexOf(uri);
|
||||
|
||||
// Get cell color
|
||||
const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex);
|
||||
const char = line.get(index);
|
||||
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||
const fg = (attr >> 9) & 0x1ff;
|
||||
|
||||
// Ensure the link is valid before registering
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
@@ -242,11 +231,11 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
return;
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
this._addLink(offset + index, rowIndex, uri, matcher, fg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
this._addLink(offset + index, rowIndex, uri, matcher, fg);
|
||||
}
|
||||
|
||||
// Recursively check for links in the rest of the text
|
||||
@@ -263,12 +252,23 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
* @param y The row the link is on.
|
||||
* @param uri The URI of the link.
|
||||
* @param matcher The link matcher for the link.
|
||||
* @param fg The link color for hover event.
|
||||
*/
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher): void {
|
||||
private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number): void {
|
||||
const x1 = x % this._terminal.cols;
|
||||
const y1 = y + Math.floor(x / this._terminal.cols);
|
||||
let x2 = (x1 + uri.length) % this._terminal.cols;
|
||||
let y2 = y1 + Math.floor((x1 + uri.length) / this._terminal.cols);
|
||||
if (x2 === 0) {
|
||||
x2 = this._terminal.cols;
|
||||
y2--;
|
||||
}
|
||||
|
||||
this._mouseZoneManager.add(new MouseZone(
|
||||
x + 1,
|
||||
x + 1 + uri.length,
|
||||
y + 1,
|
||||
x1 + 1,
|
||||
y1 + 1,
|
||||
x2 + 1,
|
||||
y2 + 1,
|
||||
e => {
|
||||
if (matcher.handler) {
|
||||
return matcher.handler(e, uri);
|
||||
@@ -276,18 +276,18 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, <ILinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = 'pointer';
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, <ILinkHoverEvent>{ x, y, length: uri.length});
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.emit(LinkHoverEventTypes.LEAVE, <ILinkHoverEvent>{ x, y, length: uri.length});
|
||||
this._terminal.element.style.cursor = '';
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
}
|
||||
@@ -300,4 +300,8 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
-674
@@ -1,674 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { C0 } from './EscapeSequences';
|
||||
import { IInputHandler } from './Types';
|
||||
import { CHARSETS, DEFAULT_CHARSET } from './Charsets';
|
||||
|
||||
const normalStateHandler: {[key: string]: (parser: Parser, handler: IInputHandler) => void} = {};
|
||||
normalStateHandler[C0.BEL] = (parser, handler) => handler.bell();
|
||||
normalStateHandler[C0.LF] = (parser, handler) => handler.lineFeed();
|
||||
normalStateHandler[C0.VT] = normalStateHandler[C0.LF];
|
||||
normalStateHandler[C0.FF] = normalStateHandler[C0.LF];
|
||||
normalStateHandler[C0.CR] = (parser, handler) => handler.carriageReturn();
|
||||
normalStateHandler[C0.BS] = (parser, handler) => handler.backspace();
|
||||
normalStateHandler[C0.HT] = (parser, handler) => handler.tab();
|
||||
normalStateHandler[C0.SO] = (parser, handler) => handler.shiftOut();
|
||||
normalStateHandler[C0.SI] = (parser, handler) => handler.shiftIn();
|
||||
normalStateHandler[C0.ESC] = (parser, handler) => parser.setState(ParserState.ESCAPED);
|
||||
|
||||
// TODO: Remove terminal when parser owns params and currentParam
|
||||
const escapedStateHandler: {[key: string]: (parser: Parser, terminal: any) => void} = {};
|
||||
escapedStateHandler['['] = (parser, terminal) => {
|
||||
// ESC [ Control Sequence Introducer (CSI is 0x9b)
|
||||
terminal.params = [];
|
||||
terminal.currentParam = 0;
|
||||
parser.setState(ParserState.CSI_PARAM);
|
||||
};
|
||||
escapedStateHandler[']'] = (parser, terminal) => {
|
||||
// ESC ] Operating System Command (OSC is 0x9d)
|
||||
terminal.params = [];
|
||||
terminal.currentParam = 0;
|
||||
parser.setState(ParserState.OSC);
|
||||
};
|
||||
escapedStateHandler['P'] = (parser, terminal) => {
|
||||
// ESC P Device Control String (DCS is 0x90)
|
||||
terminal.params = [];
|
||||
terminal.currentParam = 0;
|
||||
parser.setState(ParserState.DCS);
|
||||
};
|
||||
escapedStateHandler['_'] = (parser, terminal) => {
|
||||
// ESC _ Application Program Command ( APC is 0x9f).
|
||||
parser.setState(ParserState.IGNORE);
|
||||
};
|
||||
escapedStateHandler['^'] = (parser, terminal) => {
|
||||
// ESC ^ Privacy Message ( PM is 0x9e).
|
||||
parser.setState(ParserState.IGNORE);
|
||||
};
|
||||
escapedStateHandler['c'] = (parser, terminal) => {
|
||||
// ESC c Full Reset (RIS).
|
||||
terminal.reset();
|
||||
};
|
||||
escapedStateHandler['E'] = (parser, terminal) => {
|
||||
// ESC E Next Line ( NEL is 0x85).
|
||||
terminal.buffer.x = 0;
|
||||
terminal.index();
|
||||
parser.setState(ParserState.NORMAL);
|
||||
};
|
||||
escapedStateHandler['D'] = (parser, terminal) => {
|
||||
// ESC D Index ( IND is 0x84).
|
||||
terminal.index();
|
||||
parser.setState(ParserState.NORMAL);
|
||||
};
|
||||
escapedStateHandler['M'] = (parser, terminal) => {
|
||||
// ESC M Reverse Index ( RI is 0x8d).
|
||||
terminal.reverseIndex();
|
||||
parser.setState(ParserState.NORMAL);
|
||||
};
|
||||
escapedStateHandler['%'] = (parser, terminal) => {
|
||||
// ESC % Select default/utf-8 character set.
|
||||
// @ = default, G = utf-8
|
||||
terminal.setgLevel(0);
|
||||
terminal.setgCharset(0, DEFAULT_CHARSET); // US (default)
|
||||
parser.setState(ParserState.NORMAL);
|
||||
parser.skipNextChar();
|
||||
};
|
||||
escapedStateHandler[C0.CAN] = (parser) => parser.setState(ParserState.NORMAL);
|
||||
|
||||
const csiParamStateHandler: {[key: string]: (parser: Parser) => void} = {};
|
||||
csiParamStateHandler['?'] = (parser) => parser.setPrefix('?');
|
||||
csiParamStateHandler['>'] = (parser) => parser.setPrefix('>');
|
||||
csiParamStateHandler['!'] = (parser) => parser.setPrefix('!');
|
||||
csiParamStateHandler['0'] = (parser) => parser.setParam(parser.getParam() * 10);
|
||||
csiParamStateHandler['1'] = (parser) => parser.setParam(parser.getParam() * 10 + 1);
|
||||
csiParamStateHandler['2'] = (parser) => parser.setParam(parser.getParam() * 10 + 2);
|
||||
csiParamStateHandler['3'] = (parser) => parser.setParam(parser.getParam() * 10 + 3);
|
||||
csiParamStateHandler['4'] = (parser) => parser.setParam(parser.getParam() * 10 + 4);
|
||||
csiParamStateHandler['5'] = (parser) => parser.setParam(parser.getParam() * 10 + 5);
|
||||
csiParamStateHandler['6'] = (parser) => parser.setParam(parser.getParam() * 10 + 6);
|
||||
csiParamStateHandler['7'] = (parser) => parser.setParam(parser.getParam() * 10 + 7);
|
||||
csiParamStateHandler['8'] = (parser) => parser.setParam(parser.getParam() * 10 + 8);
|
||||
csiParamStateHandler['9'] = (parser) => parser.setParam(parser.getParam() * 10 + 9);
|
||||
csiParamStateHandler['$'] = (parser) => parser.setPostfix('$');
|
||||
csiParamStateHandler['"'] = (parser) => parser.setPostfix('"');
|
||||
csiParamStateHandler[' '] = (parser) => parser.setPostfix(' ');
|
||||
csiParamStateHandler['\''] = (parser) => parser.setPostfix('\'');
|
||||
csiParamStateHandler[';'] = (parser) => parser.finalizeParam();
|
||||
csiParamStateHandler[C0.CAN] = (parser) => parser.setState(ParserState.NORMAL);
|
||||
|
||||
const csiStateHandler: {[key: string]: (handler: IInputHandler, params: number[], prefix: string, postfix: string, parser: Parser) => void} = {};
|
||||
csiStateHandler['@'] = (handler, params, prefix) => handler.insertChars(params);
|
||||
csiStateHandler['A'] = (handler, params, prefix) => handler.cursorUp(params);
|
||||
csiStateHandler['B'] = (handler, params, prefix) => handler.cursorDown(params);
|
||||
csiStateHandler['C'] = (handler, params, prefix) => handler.cursorForward(params);
|
||||
csiStateHandler['D'] = (handler, params, prefix) => handler.cursorBackward(params);
|
||||
csiStateHandler['E'] = (handler, params, prefix) => handler.cursorNextLine(params);
|
||||
csiStateHandler['F'] = (handler, params, prefix) => handler.cursorPrecedingLine(params);
|
||||
csiStateHandler['G'] = (handler, params, prefix) => handler.cursorCharAbsolute(params);
|
||||
csiStateHandler['H'] = (handler, params, prefix) => handler.cursorPosition(params);
|
||||
csiStateHandler['I'] = (handler, params, prefix) => handler.cursorForwardTab(params);
|
||||
csiStateHandler['J'] = (handler, params, prefix) => handler.eraseInDisplay(params);
|
||||
csiStateHandler['K'] = (handler, params, prefix) => handler.eraseInLine(params);
|
||||
csiStateHandler['L'] = (handler, params, prefix) => handler.insertLines(params);
|
||||
csiStateHandler['M'] = (handler, params, prefix) => handler.deleteLines(params);
|
||||
csiStateHandler['P'] = (handler, params, prefix) => handler.deleteChars(params);
|
||||
csiStateHandler['S'] = (handler, params, prefix) => handler.scrollUp(params);
|
||||
csiStateHandler['T'] = (handler, params, prefix) => {
|
||||
if (params.length < 2 && !prefix) {
|
||||
handler.scrollDown(params);
|
||||
}
|
||||
};
|
||||
csiStateHandler['X'] = (handler, params, prefix) => handler.eraseChars(params);
|
||||
csiStateHandler['Z'] = (handler, params, prefix) => handler.cursorBackwardTab(params);
|
||||
csiStateHandler['`'] = (handler, params, prefix) => handler.charPosAbsolute(params);
|
||||
csiStateHandler['a'] = (handler, params, prefix) => handler.HPositionRelative(params);
|
||||
csiStateHandler['b'] = (handler, params, prefix) => handler.repeatPrecedingCharacter(params);
|
||||
csiStateHandler['c'] = (handler, params, prefix) => handler.sendDeviceAttributes(params);
|
||||
csiStateHandler['d'] = (handler, params, prefix) => handler.linePosAbsolute(params);
|
||||
csiStateHandler['e'] = (handler, params, prefix) => handler.VPositionRelative(params);
|
||||
csiStateHandler['f'] = (handler, params, prefix) => handler.HVPosition(params);
|
||||
csiStateHandler['g'] = (handler, params, prefix) => handler.tabClear(params);
|
||||
csiStateHandler['h'] = (handler, params, prefix) => handler.setMode(params);
|
||||
csiStateHandler['l'] = (handler, params, prefix) => handler.resetMode(params);
|
||||
csiStateHandler['m'] = (handler, params, prefix) => handler.charAttributes(params);
|
||||
csiStateHandler['n'] = (handler, params, prefix) => handler.deviceStatus(params);
|
||||
csiStateHandler['p'] = (handler, params, prefix) => {
|
||||
switch (prefix) {
|
||||
case '!': handler.softReset(params); break;
|
||||
}
|
||||
};
|
||||
csiStateHandler['q'] = (handler, params, prefix, postfix) => {
|
||||
if (postfix === ' ') {
|
||||
handler.setCursorStyle(params);
|
||||
}
|
||||
};
|
||||
csiStateHandler['r'] = (handler, params) => handler.setScrollRegion(params);
|
||||
csiStateHandler['s'] = (handler, params) => handler.saveCursor(params);
|
||||
csiStateHandler['u'] = (handler, params) => handler.restoreCursor(params);
|
||||
csiStateHandler[C0.CAN] = (handler, params, prefix, postfix, parser) => parser.setState(ParserState.NORMAL);
|
||||
|
||||
export enum ParserState {
|
||||
NORMAL = 0,
|
||||
ESCAPED = 1,
|
||||
CSI_PARAM = 2,
|
||||
CSI = 3,
|
||||
OSC = 4,
|
||||
CHARSET = 5,
|
||||
DCS = 6,
|
||||
IGNORE = 7
|
||||
}
|
||||
|
||||
/**
|
||||
* The terminal's parser, all input into the terminal goes through the parser
|
||||
* which parses and defers the actual input handling the the IInputHandler
|
||||
* specified in the constructor.
|
||||
*/
|
||||
export class Parser {
|
||||
private _state: ParserState;
|
||||
private _position: number;
|
||||
|
||||
// TODO: Remove terminal when handler can do everything
|
||||
constructor(
|
||||
private _inputHandler: IInputHandler,
|
||||
private _terminal: any
|
||||
) {
|
||||
this._state = ParserState.NORMAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse and handle data.
|
||||
*
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
public parse(data: string): ParserState {
|
||||
const l = data.length;
|
||||
let j;
|
||||
let cs;
|
||||
let ch;
|
||||
let code;
|
||||
let low;
|
||||
|
||||
const cursorStartX = this._terminal.buffer.x;
|
||||
const cursorStartY = this._terminal.buffer.y;
|
||||
|
||||
if (this._terminal.debug) {
|
||||
this._terminal.log('data: ' + data);
|
||||
}
|
||||
|
||||
this._position = 0;
|
||||
// apply leftover surrogate high from last write
|
||||
if (this._terminal.surrogate_high) {
|
||||
data = this._terminal.surrogate_high + data;
|
||||
this._terminal.surrogate_high = '';
|
||||
}
|
||||
|
||||
for (; this._position < l; this._position++) {
|
||||
ch = data[this._position];
|
||||
|
||||
// FIXME: higher chars than 0xa0 are not allowed in escape sequences
|
||||
// --> maybe move to default
|
||||
code = data.charCodeAt(this._position);
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
// we got a surrogate high
|
||||
// get surrogate low (next 2 bytes)
|
||||
low = data.charCodeAt(this._position + 1);
|
||||
if (isNaN(low)) {
|
||||
// end of data stream, save surrogate high
|
||||
this._terminal.surrogate_high = ch;
|
||||
continue;
|
||||
}
|
||||
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
||||
ch += data.charAt(this._position + 1);
|
||||
}
|
||||
// surrogate low - already handled above
|
||||
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (this._state) {
|
||||
case ParserState.NORMAL:
|
||||
if (ch in normalStateHandler) {
|
||||
normalStateHandler[ch](this, this._inputHandler);
|
||||
} else {
|
||||
this._inputHandler.addChar(ch, code);
|
||||
}
|
||||
break;
|
||||
case ParserState.ESCAPED:
|
||||
if (ch in escapedStateHandler) {
|
||||
escapedStateHandler[ch](this, this._terminal);
|
||||
// Skip switch as it was just handled
|
||||
break;
|
||||
}
|
||||
switch (ch) {
|
||||
|
||||
// ESC (,),*,+,-,. Designate G0-G2 Character Set.
|
||||
case '(': // <-- this seems to get all the attention
|
||||
case ')':
|
||||
case '*':
|
||||
case '+':
|
||||
case '-':
|
||||
case '.':
|
||||
switch (ch) {
|
||||
case '(':
|
||||
this._terminal.gcharset = 0;
|
||||
break;
|
||||
case ')':
|
||||
this._terminal.gcharset = 1;
|
||||
break;
|
||||
case '*':
|
||||
this._terminal.gcharset = 2;
|
||||
break;
|
||||
case '+':
|
||||
this._terminal.gcharset = 3;
|
||||
break;
|
||||
case '-':
|
||||
this._terminal.gcharset = 1;
|
||||
break;
|
||||
case '.':
|
||||
this._terminal.gcharset = 2;
|
||||
break;
|
||||
}
|
||||
this._state = ParserState.CHARSET;
|
||||
break;
|
||||
|
||||
// Designate G3 Character Set (VT300).
|
||||
// A = ISO Latin-1 Supplemental.
|
||||
// Not implemented.
|
||||
case '/':
|
||||
this._terminal.gcharset = 3;
|
||||
this._state = ParserState.CHARSET;
|
||||
this._position--;
|
||||
break;
|
||||
|
||||
// ESC N
|
||||
// Single Shift Select of G2 Character Set
|
||||
// ( SS2 is 0x8e). This affects next character only.
|
||||
case 'N':
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC O
|
||||
// Single Shift Select of G3 Character Set
|
||||
// ( SS3 is 0x8f). This affects next character only.
|
||||
case 'O':
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC n
|
||||
// Invoke the G2 Character Set as GL (LS2).
|
||||
case 'n':
|
||||
this._terminal.setgLevel(2);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC o
|
||||
// Invoke the G3 Character Set as GL (LS3).
|
||||
case 'o':
|
||||
this._terminal.setgLevel(3);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC |
|
||||
// Invoke the G3 Character Set as GR (LS3R).
|
||||
case '|':
|
||||
this._terminal.setgLevel(3);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC }
|
||||
// Invoke the G2 Character Set as GR (LS2R).
|
||||
case '}':
|
||||
this._terminal.setgLevel(2);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
// ESC ~
|
||||
// Invoke the G1 Character Set as GR (LS1R).
|
||||
case '~':
|
||||
this._terminal.setgLevel(1);
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC 7 Save Cursor (DECSC).
|
||||
case '7':
|
||||
this._inputHandler.saveCursor();
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC 8 Restore Cursor (DECRC).
|
||||
case '8':
|
||||
this._inputHandler.restoreCursor();
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC # 3 DEC line height/width
|
||||
case '#':
|
||||
this._state = ParserState.NORMAL;
|
||||
this._position++;
|
||||
break;
|
||||
|
||||
// ESC H Tab Set (HTS is 0x88).
|
||||
case 'H':
|
||||
this._terminal.tabSet();
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC = Application Keypad (DECKPAM).
|
||||
case '=':
|
||||
this._terminal.log('Serial port requested application keypad.');
|
||||
this._terminal.applicationKeypad = true;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
}
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
// ESC > Normal Keypad (DECKPNM).
|
||||
case '>':
|
||||
this._terminal.log('Switching back to normal keypad.');
|
||||
this._terminal.applicationKeypad = false;
|
||||
if (this._terminal.viewport) {
|
||||
this._terminal.viewport.syncScrollArea();
|
||||
}
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
default:
|
||||
this._state = ParserState.NORMAL;
|
||||
this._terminal.error('Unknown ESC control: %s.', ch);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
|
||||
case ParserState.CHARSET:
|
||||
if (ch in CHARSETS) {
|
||||
cs = CHARSETS[ch];
|
||||
if (ch === '/') { // ISOLatin is actually /A
|
||||
this.skipNextChar();
|
||||
}
|
||||
} else {
|
||||
cs = DEFAULT_CHARSET;
|
||||
}
|
||||
this._terminal.setgCharset(this._terminal.gcharset, cs);
|
||||
this._terminal.gcharset = null;
|
||||
this._state = ParserState.NORMAL;
|
||||
break;
|
||||
|
||||
case ParserState.OSC:
|
||||
// OSC Ps ; Pt ST
|
||||
// OSC Ps ; Pt BEL
|
||||
// Set Text Parameters.
|
||||
if (ch === C0.ESC || ch === C0.BEL) {
|
||||
if (ch === C0.ESC) this._position++;
|
||||
|
||||
this._terminal.params.push(this._terminal.currentParam);
|
||||
|
||||
switch (this._terminal.params[0]) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
if (this._terminal.params[1]) {
|
||||
this._terminal.title = this._terminal.params[1];
|
||||
this._terminal.handleTitle(this._terminal.title);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
// set X property
|
||||
break;
|
||||
case 4:
|
||||
case 5:
|
||||
// change dynamic colors
|
||||
break;
|
||||
case 10:
|
||||
case 11:
|
||||
case 12:
|
||||
case 13:
|
||||
case 14:
|
||||
case 15:
|
||||
case 16:
|
||||
case 17:
|
||||
case 18:
|
||||
case 19:
|
||||
// change dynamic ui colors
|
||||
break;
|
||||
case 46:
|
||||
// change log file
|
||||
break;
|
||||
case 50:
|
||||
// dynamic font
|
||||
break;
|
||||
case 51:
|
||||
// emacs shell
|
||||
break;
|
||||
case 52:
|
||||
// manipulate selection data
|
||||
break;
|
||||
case 104:
|
||||
case 105:
|
||||
case 110:
|
||||
case 111:
|
||||
case 112:
|
||||
case 113:
|
||||
case 114:
|
||||
case 115:
|
||||
case 116:
|
||||
case 117:
|
||||
case 118:
|
||||
// reset colors
|
||||
break;
|
||||
}
|
||||
|
||||
this._terminal.params = [];
|
||||
this._terminal.currentParam = 0;
|
||||
this._state = ParserState.NORMAL;
|
||||
} else {
|
||||
if (!this._terminal.params.length) {
|
||||
if (ch >= '0' && ch <= '9') {
|
||||
this._terminal.currentParam =
|
||||
this._terminal.currentParam * 10 + ch.charCodeAt(0) - 48;
|
||||
} else if (ch === ';') {
|
||||
this._terminal.params.push(this._terminal.currentParam);
|
||||
this._terminal.currentParam = '';
|
||||
}
|
||||
} else {
|
||||
this._terminal.currentParam += ch;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ParserState.CSI_PARAM:
|
||||
if (ch in csiParamStateHandler) {
|
||||
csiParamStateHandler[ch](this);
|
||||
break;
|
||||
}
|
||||
this.finalizeParam();
|
||||
// Fall through the CSI as this character should be the CSI code.
|
||||
this._state = ParserState.CSI;
|
||||
|
||||
case ParserState.CSI:
|
||||
if (ch in csiStateHandler) {
|
||||
if (this._terminal.debug) {
|
||||
this._terminal.log(`CSI ${this._terminal.prefix ? this._terminal.prefix : ''} ${this._terminal.params ? this._terminal.params.join(';') : ''} ${this._terminal.postfix ? this._terminal.postfix : ''} ${ch}`);
|
||||
}
|
||||
csiStateHandler[ch](this._inputHandler, this._terminal.params, this._terminal.prefix, this._terminal.postfix, this);
|
||||
} else {
|
||||
this._terminal.error('Unknown CSI code: %s.', ch);
|
||||
}
|
||||
|
||||
this._state = ParserState.NORMAL;
|
||||
this._terminal.prefix = '';
|
||||
this._terminal.postfix = '';
|
||||
break;
|
||||
|
||||
case ParserState.DCS:
|
||||
if (ch === C0.ESC || ch === C0.BEL) {
|
||||
if (ch === C0.ESC) this._position++;
|
||||
let pt;
|
||||
let valid: boolean;
|
||||
|
||||
switch (this._terminal.prefix) {
|
||||
// User-Defined Keys (DECUDK).
|
||||
case '':
|
||||
break;
|
||||
|
||||
// Request Status String (DECRQSS).
|
||||
// test: echo -e '\eP$q"p\e\\'
|
||||
case '$q':
|
||||
pt = this._terminal.currentParam;
|
||||
valid = false;
|
||||
|
||||
switch (pt) {
|
||||
// DECSCA
|
||||
case '"q':
|
||||
pt = '0"q';
|
||||
break;
|
||||
|
||||
// DECSCL
|
||||
case '"p':
|
||||
pt = '61"p';
|
||||
break;
|
||||
|
||||
// DECSTBM
|
||||
case 'r':
|
||||
pt = ''
|
||||
+ (this._terminal.buffer.scrollTop + 1)
|
||||
+ ';'
|
||||
+ (this._terminal.buffer.scrollBottom + 1)
|
||||
+ 'r';
|
||||
break;
|
||||
|
||||
// SGR
|
||||
case 'm':
|
||||
pt = '0m';
|
||||
break;
|
||||
|
||||
default:
|
||||
this._terminal.error('Unknown DCS Pt: %s.', pt);
|
||||
pt = '';
|
||||
break;
|
||||
}
|
||||
|
||||
this._terminal.send(C0.ESC + 'P' + +valid + '$r' + pt + C0.ESC + '\\');
|
||||
break;
|
||||
|
||||
// Set Termcap/Terminfo Data (xterm, experimental).
|
||||
case '+p':
|
||||
break;
|
||||
|
||||
// Request Termcap/Terminfo String (xterm, experimental)
|
||||
// Regular xterm does not even respond to this sequence.
|
||||
// This can cause a small glitch in vim.
|
||||
// test: echo -ne '\eP+q6b64\e\\'
|
||||
case '+q':
|
||||
pt = this._terminal.currentParam;
|
||||
valid = false;
|
||||
|
||||
this._terminal.send(C0.ESC + 'P' + +valid + '+r' + pt + C0.ESC + '\\');
|
||||
break;
|
||||
|
||||
default:
|
||||
this._terminal.error('Unknown DCS prefix: %s.', this._terminal.prefix);
|
||||
break;
|
||||
}
|
||||
|
||||
this._terminal.currentParam = 0;
|
||||
this._terminal.prefix = '';
|
||||
this._state = ParserState.NORMAL;
|
||||
} else if (!this._terminal.currentParam) {
|
||||
if (!this._terminal.prefix && ch !== '$' && ch !== '+') {
|
||||
this._terminal.currentParam = ch;
|
||||
} else if (this._terminal.prefix.length === 2) {
|
||||
this._terminal.currentParam = ch;
|
||||
} else {
|
||||
this._terminal.prefix += ch;
|
||||
}
|
||||
} else {
|
||||
this._terminal.currentParam += ch;
|
||||
}
|
||||
break;
|
||||
|
||||
case ParserState.IGNORE:
|
||||
// For PM and APC.
|
||||
if (ch === C0.ESC || ch === C0.BEL) {
|
||||
if (ch === C0.ESC) this._position++;
|
||||
this._state = ParserState.NORMAL;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fire the cursormove event if it's moved. This is done inside the parser
|
||||
// as a render cannot happen in the middle of a parsing round.
|
||||
if (this._terminal.buffer.x !== cursorStartX || this._terminal.buffer.y !== cursorStartY) {
|
||||
this._terminal.emit('cursormove');
|
||||
}
|
||||
|
||||
return this._state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the parser's current parsing state.
|
||||
*
|
||||
* @param state The new state.
|
||||
*/
|
||||
public setState(state: ParserState): void {
|
||||
this._state = state;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parsier's current prefix. CSI codes can have prefixes of '?', '>'
|
||||
* or '!'.
|
||||
*
|
||||
* @param prefix The prefix.
|
||||
*/
|
||||
public setPrefix(prefix: string): void {
|
||||
this._terminal.prefix = prefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parsier's current prefix. CSI codes can have postfixes of '$',
|
||||
* '"', ' ', '\''.
|
||||
*
|
||||
* @param postfix The postfix.
|
||||
*/
|
||||
public setPostfix(postfix: string): void {
|
||||
this._terminal.postfix = postfix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the parser's current parameter.
|
||||
*
|
||||
* @param param the parameter.
|
||||
*/
|
||||
public setParam(param: number): void {
|
||||
this._terminal.currentParam = param;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the parser's current parameter.
|
||||
*/
|
||||
public getParam(): number {
|
||||
return this._terminal.currentParam;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes the parser's current parameter, adding it to the list of
|
||||
* parameters and setting the new current parameter to 0.
|
||||
*/
|
||||
public finalizeParam(): void {
|
||||
this._terminal.params.push(this._terminal.currentParam);
|
||||
this._terminal.currentParam = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the parser to skip the next character.
|
||||
*/
|
||||
public skipNextChar(): void {
|
||||
this._position++;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell the parser to repeat parsing the current character (for example if it
|
||||
* needs parsing using a different state.
|
||||
*/
|
||||
// public repeatChar(): void {
|
||||
// this._position--;
|
||||
// }
|
||||
}
|
||||
+159
-19
@@ -3,15 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { SelectionManager } from './SelectionManager';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { SelectionManager, SelectionMode } from './SelectionManager';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { BufferSet } from './BufferSet';
|
||||
import { LineData, CharData, ITerminal, ICircularList, IBuffer } from './Types';
|
||||
import { ITerminal, IBuffer, IBufferLine } from './Types';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestMockTerminal extends MockTerminal {
|
||||
emit(event: string, data: any): void {}
|
||||
@@ -27,6 +26,8 @@ class TestSelectionManager extends SelectionManager {
|
||||
|
||||
public get model(): SelectionModel { return this._model; }
|
||||
|
||||
public set selectionMode(mode: SelectionMode) { this._activeSelectionMode = mode; }
|
||||
|
||||
public selectLineAt(line: number): void { this._selectLineAt(line); }
|
||||
public selectWordAt(coords: [number, number]): void { this._selectWordAt(coords, true); }
|
||||
|
||||
@@ -37,19 +38,11 @@ class TestSelectionManager extends SelectionManager {
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let buffer: IBuffer;
|
||||
let rowContainer: HTMLElement;
|
||||
let selectionManager: TestSelectionManager;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
terminal = new TestMockTerminal();
|
||||
terminal.cols = 80;
|
||||
terminal.rows = 2;
|
||||
@@ -60,16 +53,18 @@ describe('SelectionManager', () => {
|
||||
selectionManager = new TestSelectionManager(terminal, null);
|
||||
});
|
||||
|
||||
function stringToRow(text: string): LineData {
|
||||
let result: LineData = [];
|
||||
function stringToRow(text: string): IBufferLine {
|
||||
const result = new BufferLine();
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
result.push([0, text.charAt(i), 1, text.charCodeAt(i)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function stringArrayToRow(chars: string[]): LineData {
|
||||
return chars.map(c => <CharData>[0, c, 1, c.charCodeAt(0)]);
|
||||
function stringArrayToRow(chars: string[]): IBufferLine {
|
||||
const line = new BufferLine();
|
||||
chars.map(c => line.push([0, c, 1, c.charCodeAt(0)]));
|
||||
return line;
|
||||
}
|
||||
|
||||
describe('_selectWordAt', () => {
|
||||
@@ -105,7 +100,8 @@ describe('SelectionManager', () => {
|
||||
});
|
||||
it('should expand selection for wide characters', () => {
|
||||
// Wide characters use a special format
|
||||
buffer.lines.set(0, [
|
||||
const line = new BufferLine();
|
||||
const data: [number, string, number, number][] = [
|
||||
[null, '中', 2, '中'.charCodeAt(0)],
|
||||
[null, '', 0, null],
|
||||
[null, '文', 2, '文'.charCodeAt(0)],
|
||||
@@ -121,7 +117,9 @@ describe('SelectionManager', () => {
|
||||
[null, 'f', 1, 'f'.charCodeAt(0)],
|
||||
[null, 'o', 1, 'o'.charCodeAt(0)],
|
||||
[null, 'o', 1, 'o'.charCodeAt(0)]
|
||||
]);
|
||||
];
|
||||
for (let i = 0; i < data.length; ++i) line.push(data[i]);
|
||||
buffer.lines.set(0, line);
|
||||
// Ensure wide characters take up 2 columns
|
||||
selectionManager.selectWordAt([0, 0]);
|
||||
assert.equal(selectionManager.selectionText, '中文');
|
||||
@@ -191,6 +189,42 @@ describe('SelectionManager', () => {
|
||||
selectionManager.selectWordAt([15, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'ij"');
|
||||
});
|
||||
it('should expand upwards or downards for wrapped lines', () => {
|
||||
buffer.lines.set(0, stringToRow(' foo'));
|
||||
buffer.lines.set(1, stringToRow('bar '));
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
selectionManager.selectWordAt([1, 1]);
|
||||
assert.equal(selectionManager.selectionText, 'foobar');
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([78, 0]);
|
||||
assert.equal(selectionManager.selectionText, 'foobar');
|
||||
});
|
||||
it('should expand both upwards and downwards for word wrapped over many lines', () => {
|
||||
const expectedText = 'fooaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccbar';
|
||||
buffer.lines.set(0, stringToRow(' foo'));
|
||||
buffer.lines.set(1, stringToRow('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
||||
buffer.lines.set(2, stringToRow('bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'));
|
||||
buffer.lines.set(3, stringToRow('cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'));
|
||||
buffer.lines.set(4, stringToRow('bar '));
|
||||
buffer.lines.get(1).isWrapped = true;
|
||||
buffer.lines.get(2).isWrapped = true;
|
||||
buffer.lines.get(3).isWrapped = true;
|
||||
buffer.lines.get(4).isWrapped = true;
|
||||
selectionManager.selectWordAt([78, 0]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 1]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 2]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([40, 3]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
selectionManager.model.clearSelection();
|
||||
selectionManager.selectWordAt([1, 4]);
|
||||
assert.equal(selectionManager.selectionText, expectedText);
|
||||
});
|
||||
describe('emoji', () => {
|
||||
it('should treat a single emoji as a word when wrapped in spaces', () => {
|
||||
buffer.lines.set(0, stringToRow(' ⚽ a')); // The a is here to prevent the space being trimmed in selectionText
|
||||
@@ -308,6 +342,16 @@ describe('SelectionManager', () => {
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0], 'The actual selection spans the entire column');
|
||||
});
|
||||
it('should select the entire wrapped line', () => {
|
||||
buffer.lines.set(0, stringToRow('foo'));
|
||||
const line2 = stringToRow('bar');
|
||||
line2.isWrapped = true;
|
||||
buffer.lines.set(1, line2);
|
||||
selectionManager.selectLineAt(0);
|
||||
assert.equal(selectionManager.selectionText, 'foobar', 'The selected text is correct');
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1], 'The actual selection spans the entire column');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectAll', () => {
|
||||
@@ -324,6 +368,45 @@ describe('SelectionManager', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectLines', () => {
|
||||
it('should select a single line', () => {
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
buffer.lines.set(2, stringToRow('3'));
|
||||
selectionManager.selectLines(1, 1);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
|
||||
});
|
||||
it('should select multiple lines', () => {
|
||||
buffer.lines.length = 5;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
buffer.lines.set(2, stringToRow('3'));
|
||||
buffer.lines.set(3, stringToRow('4'));
|
||||
buffer.lines.set(4, stringToRow('5'));
|
||||
selectionManager.selectLines(1, 3);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 3]);
|
||||
});
|
||||
it('should select the to the start when requesting a negative row', () => {
|
||||
buffer.lines.length = 2;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
selectionManager.selectLines(-1, 0);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 0]);
|
||||
});
|
||||
it('should select the to the end when requesting beyond the final row', () => {
|
||||
buffer.lines.length = 2;
|
||||
buffer.lines.set(0, stringToRow('1'));
|
||||
buffer.lines.set(1, stringToRow('2'));
|
||||
selectionManager.selectLines(1, 2);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionStart, [0, 1]);
|
||||
assert.deepEqual(selectionManager.model.finalSelectionEnd, [terminal.cols, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasSelection', () => {
|
||||
it('should return whether there is a selection', () => {
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
@@ -339,4 +422,61 @@ describe('SelectionManager', () => {
|
||||
assert.equal(selectionManager.hasSelection, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('column selection', () => {
|
||||
it('should select a column of text', () => {
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('abcdefghij'));
|
||||
buffer.lines.set(1, stringToRow('klmnopqrst'));
|
||||
buffer.lines.set(2, stringToRow('uvwxyz'));
|
||||
|
||||
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||
selectionManager.model.selectionStart = [2, 0];
|
||||
selectionManager.model.selectionEnd = [4, 2];
|
||||
|
||||
assert.equal(selectionManager.selectionText, 'cd\nmn\nwx');
|
||||
});
|
||||
|
||||
it('should select a column of text without chopping up double width characters', () => {
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('a'));
|
||||
buffer.lines.set(1, stringToRow('語'));
|
||||
buffer.lines.set(2, stringToRow('b'));
|
||||
|
||||
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
selectionManager.model.selectionEnd = [1, 2];
|
||||
|
||||
assert.equal(selectionManager.selectionText, 'a\n語\nb');
|
||||
});
|
||||
|
||||
it('should select a column of text with single character emojis', () => {
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('a'));
|
||||
buffer.lines.set(1, stringToRow('☃'));
|
||||
buffer.lines.set(2, stringToRow('c'));
|
||||
|
||||
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
selectionManager.model.selectionEnd = [1, 2];
|
||||
|
||||
assert.equal(selectionManager.selectionText, 'a\n☃\nc');
|
||||
});
|
||||
|
||||
it('should select a column of text with double character emojis', () => {
|
||||
// TODO the case this is testing works for me in the demo webapp,
|
||||
// but doing it programmatically fails.
|
||||
buffer.lines.length = 3;
|
||||
buffer.lines.set(0, stringToRow('a '));
|
||||
buffer.lines.set(1, stringArrayToRow(['😁', ' ']));
|
||||
buffer.lines.set(2, stringToRow('c '));
|
||||
|
||||
selectionManager.selectionMode = SelectionMode.COLUMN;
|
||||
selectionManager.model.selectionStart = [0, 0];
|
||||
selectionManager.model.selectionEnd = [1, 2];
|
||||
|
||||
assert.equal(selectionManager.selectionText, 'a\n😁\nc');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+175
-60
@@ -3,14 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, ISelectionManager, IBuffer, LineData, CharData } from './Types';
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener, IBufferLine } from './Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { SelectionModel } from './SelectionModel';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX } from './Buffer';
|
||||
import { AltClickHandler } from './handlers/AltClickHandler';
|
||||
|
||||
/**
|
||||
@@ -55,10 +54,11 @@ interface IWordPosition {
|
||||
/**
|
||||
* A selection mode, this drives how the selection behaves on mouse move.
|
||||
*/
|
||||
enum SelectionMode {
|
||||
export const enum SelectionMode {
|
||||
NORMAL,
|
||||
WORD,
|
||||
LINE
|
||||
LINE,
|
||||
COLUMN
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,7 +81,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
/**
|
||||
* The current selection mode.
|
||||
*/
|
||||
private _activeSelectionMode: SelectionMode;
|
||||
protected _activeSelectionMode: SelectionMode;
|
||||
|
||||
/**
|
||||
* A setInterval timer that is active while the mouse is down whose callback
|
||||
@@ -101,7 +101,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
private _mouseMoveListener: EventListener;
|
||||
private _mouseUpListener: EventListener;
|
||||
private _trimListener: (...args: any[]) => void;
|
||||
private _trimListener: XtermListener;
|
||||
|
||||
private _mouseDownTimeStamp: number;
|
||||
|
||||
@@ -117,6 +117,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._removeMouseDownListeners();
|
||||
}
|
||||
|
||||
private get _buffer(): IBuffer {
|
||||
return this._terminal.buffers.active;
|
||||
}
|
||||
@@ -178,30 +183,43 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
return '';
|
||||
}
|
||||
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||
let result: string[] = [];
|
||||
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||
const result: string[] = [];
|
||||
|
||||
// Get middle rows
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
const bufferLine = this._buffer.lines.get(i);
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
if (this._activeSelectionMode === SelectionMode.COLUMN) {
|
||||
// Ignore zero width selections
|
||||
if (start[0] === end[0]) {
|
||||
return '';
|
||||
}
|
||||
|
||||
for (let i = start[1]; i <= end[1]; i++) {
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true, start[0], end[0]);
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Get first row
|
||||
const startRowEndCol = start[1] === end[1] ? end[0] : null;
|
||||
result.push(this._buffer.translateBufferLineToString(start[1], true, start[0], startRowEndCol));
|
||||
|
||||
// Get final row
|
||||
if (start[1] !== end[1]) {
|
||||
const bufferLine = this._buffer.lines.get(end[1]);
|
||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||
if ((<any>bufferLine).isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
// Get middle rows
|
||||
for (let i = start[1] + 1; i <= end[1] - 1; i++) {
|
||||
const bufferLine = this._buffer.lines.get(i);
|
||||
const lineText = this._buffer.translateBufferLineToString(i, true);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
|
||||
// Get final row
|
||||
if (start[1] !== end[1]) {
|
||||
const bufferLine = this._buffer.lines.get(end[1]);
|
||||
const lineText = this._buffer.translateBufferLineToString(end[1], true, 0, end[0]);
|
||||
if (bufferLine.isWrapped) {
|
||||
result[result.length - 1] += lineText;
|
||||
} else {
|
||||
result.push(lineText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,7 +268,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
*/
|
||||
private _refresh(): void {
|
||||
this._refreshAnimationFrame = null;
|
||||
this.emit('refresh', { start: this._model.finalSelectionStart, end: this._model.finalSelectionEnd });
|
||||
this.emit('refresh', {
|
||||
start: this._model.finalSelectionStart,
|
||||
end: this._model.finalSelectionEnd,
|
||||
columnSelectMode: this._activeSelectionMode === SelectionMode.COLUMN
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -293,6 +315,16 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
public selectLines(start: number, end: number): void {
|
||||
this._model.clearSelection();
|
||||
start = Math.max(start, 0);
|
||||
end = Math.min(end, this._terminal.buffer.lines.length - 1);
|
||||
this._model.selectionStart = [0, start];
|
||||
this._model.selectionEnd = [this._terminal.cols, end];
|
||||
this.refresh();
|
||||
this._terminal.emit('selection');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
@@ -349,7 +381,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
public shouldForceSelection(event: MouseEvent): boolean {
|
||||
return Browser.isMac ? event.altKey : event.shiftKey;
|
||||
if (Browser.isMac) {
|
||||
return event.altKey && this._terminal.options.macOptionClickForcesSelection;
|
||||
}
|
||||
|
||||
return event.shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -415,8 +451,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Removes the listeners that are registered when mousedown is triggered.
|
||||
*/
|
||||
private _removeMouseDownListeners(): void {
|
||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
if (this._terminal.element.ownerDocument) {
|
||||
this._terminal.element.ownerDocument.removeEventListener('mousemove', this._mouseMoveListener);
|
||||
this._terminal.element.ownerDocument.removeEventListener('mouseup', this._mouseUpListener);
|
||||
}
|
||||
clearInterval(this._dragScrollIntervalTimer);
|
||||
this._dragScrollIntervalTimer = null;
|
||||
}
|
||||
@@ -440,7 +478,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _onSingleClick(event: MouseEvent): void {
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.isSelectAllActive = false;
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
|
||||
|
||||
// Initialize the new selection
|
||||
this._model.selectionStart = this._getMouseBufferCoords(event);
|
||||
@@ -462,7 +500,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// If the mouse is over the second half of a wide character, adjust the
|
||||
// selection to cover the whole character
|
||||
const char = line[this._model.selectionStart[0]];
|
||||
const char = line.get(this._model.selectionStart[0]);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionStart[0]++;
|
||||
}
|
||||
@@ -493,6 +531,14 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the selection manager should operate in column select mode
|
||||
* @param event the mouse or keyboard event
|
||||
*/
|
||||
public shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean {
|
||||
return event.altKey && !(Browser.isMac && this._terminal.options.macOptionClickForcesSelection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the mousemove event when the mouse button is down, recording the
|
||||
* end of the selection and refreshing the selection.
|
||||
@@ -530,18 +576,21 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
this._dragScrollAmount = this._getMouseEventScrollAmount(event);
|
||||
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively.
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
// start or end of the viewport respectively. This should only happen when
|
||||
// NOT in column select mode.
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
} else if (this._dragScrollAmount < 0) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If the character is a wide character include the cell to the right in the
|
||||
// selection. Note that selections at the very end of the line will never
|
||||
// have a character.
|
||||
if (this._model.selectionEnd[1] < this._buffer.lines.length) {
|
||||
const char = this._buffer.lines.get(this._model.selectionEnd[1])[this._model.selectionEnd[0]];
|
||||
const char = this._buffer.lines.get(this._model.selectionEnd[1]).get(this._model.selectionEnd[0]);
|
||||
if (char && char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
this._model.selectionEnd[0]++;
|
||||
}
|
||||
@@ -563,10 +612,19 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
if (this._dragScrollAmount) {
|
||||
this._terminal.scrollLines(this._dragScrollAmount, false);
|
||||
// Re-evaluate selection
|
||||
// If the cursor was above or below the viewport, make sure it's at the
|
||||
// start or end of the viewport respectively. This should only happen when
|
||||
// NOT in column select mode.
|
||||
if (this._dragScrollAmount > 0) {
|
||||
this._model.selectionEnd = [this._terminal.cols - 1, Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1)];
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = this._terminal.cols;
|
||||
}
|
||||
this._model.selectionEnd[1] = Math.min(this._terminal.buffer.ydisp + this._terminal.rows, this._terminal.buffer.lines.length - 1);
|
||||
} else {
|
||||
this._model.selectionEnd = [0, this._terminal.buffer.ydisp];
|
||||
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
|
||||
this._model.selectionEnd[0] = 0;
|
||||
}
|
||||
this._model.selectionEnd[1] = this._terminal.buffer.ydisp;
|
||||
}
|
||||
this.refresh();
|
||||
}
|
||||
@@ -577,7 +635,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent): void {
|
||||
let timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
|
||||
@@ -603,10 +661,10 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* latter takes into account wide characters.
|
||||
* @param coords The coordinates to find the 2 index for.
|
||||
*/
|
||||
private _convertViewportColToCharacterIndex(bufferLine: any, coords: [number, number]): number {
|
||||
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
const char = bufferLine[i];
|
||||
const char = bufferLine.get(i);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
@@ -633,7 +691,12 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* Gets positional information for the word at the coordinated specified.
|
||||
* @param coords The coordinates to get the word at.
|
||||
*/
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): IWordPosition {
|
||||
private _getWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean, followWrappedLinesAbove: boolean = true, followWrappedLinesBelow: boolean = true): IWordPosition {
|
||||
// Ensure coords are within viewport (eg. not within scroll bar)
|
||||
if (coords[0] >= this._terminal.cols) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bufferLine = this._buffer.lines.get(coords[1]);
|
||||
if (!bufferLine) {
|
||||
return null;
|
||||
@@ -670,24 +733,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Consider the initial position, skip it and increment the wide char
|
||||
// variable
|
||||
if (bufferLine[startCol][CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
if (bufferLine.get(startCol)[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
leftWideCharCount++;
|
||||
startCol--;
|
||||
}
|
||||
if (bufferLine[endCol][CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
rightWideCharCount++;
|
||||
endCol++;
|
||||
}
|
||||
|
||||
// Adjust the end index for characters whose length are > 1 (emojis)
|
||||
if (bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine[endCol][CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
if (bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length > 1) {
|
||||
rightLongCharOffset += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
endIndex += bufferLine.get(endCol)[CHAR_DATA_CHAR_INDEX].length - 1;
|
||||
}
|
||||
|
||||
// Expand the string in both directions until a space is hit
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine[startCol - 1])) {
|
||||
const char = bufferLine[startCol - 1];
|
||||
while (startCol > 0 && startIndex > 0 && !this._isCharWordSeparator(bufferLine.get(startCol - 1))) {
|
||||
const char = bufferLine.get(startCol - 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 0) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
leftWideCharCount++;
|
||||
@@ -701,8 +764,8 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
startIndex--;
|
||||
startCol--;
|
||||
}
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine[endCol + 1])) {
|
||||
const char = bufferLine[endCol + 1];
|
||||
while (endCol < bufferLine.length && endIndex + 1 < line.length && !this._isCharWordSeparator(bufferLine.get(endCol + 1))) {
|
||||
const char = bufferLine.get(endCol + 1);
|
||||
if (char[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// If the next character is a wide char, record it and skip the column
|
||||
rightWideCharCount++;
|
||||
@@ -723,7 +786,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Calculate the start _column_, converting the the string indexes back to
|
||||
// column coordinates.
|
||||
const start =
|
||||
let start =
|
||||
startIndex // The index of the selection's start char in the line string
|
||||
+ charOffset // The difference between the initial char's column and index
|
||||
- leftWideCharCount // The number of wide chars left of the initial char
|
||||
@@ -731,7 +794,7 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
|
||||
// Calculate the length in _columns_, converting the the string indexes back
|
||||
// to column coordinates.
|
||||
const length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||
let length = Math.min(this._terminal.cols, // Disallow lengths larger than the terminal cols
|
||||
endIndex // The index of the selection's end char in the line string
|
||||
- startIndex // The index of the selection's start char in the line string
|
||||
+ leftWideCharCount // The number of wide chars left of the initial char
|
||||
@@ -743,6 +806,34 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recurse upwards if the line is wrapped and the word wraps to the above line
|
||||
if (followWrappedLinesAbove) {
|
||||
if (start === 0 && bufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const previousBufferLine = this._buffer.lines.get(coords[1] - 1);
|
||||
if (previousBufferLine && bufferLine.isWrapped && previousBufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const previousLineWordPosition = this._getWordAt([this._terminal.cols - 1, coords[1] - 1], false, true, false);
|
||||
if (previousLineWordPosition) {
|
||||
const offset = this._terminal.cols - previousLineWordPosition.start;
|
||||
start -= offset;
|
||||
length += offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recurse downwards if the line is wrapped and the word wraps to the next line
|
||||
if (followWrappedLinesBelow) {
|
||||
if (start + length === this._terminal.cols && bufferLine.get(this._terminal.cols - 1)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const nextBufferLine = this._buffer.lines.get(coords[1] + 1);
|
||||
if (nextBufferLine && nextBufferLine.isWrapped && nextBufferLine.get(0)[CHAR_DATA_CODE_INDEX] !== 32 /*' '*/) {
|
||||
const nextLineWordPosition = this._getWordAt([0, coords[1] + 1], false, false, true);
|
||||
if (nextLineWordPosition) {
|
||||
length += nextLineWordPosition.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { start, length };
|
||||
}
|
||||
|
||||
@@ -754,6 +845,11 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
protected _selectWordAt(coords: [number, number], allowWhitespaceOnlySelection: boolean): void {
|
||||
const wordPosition = this._getWordAt(coords, allowWhitespaceOnlySelection);
|
||||
if (wordPosition) {
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
coords[1]--;
|
||||
}
|
||||
this._model.selectionStart = [wordPosition.start, coords[1]];
|
||||
this._model.selectionStartLength = wordPosition.length;
|
||||
}
|
||||
@@ -766,7 +862,24 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
private _selectToWordAt(coords: [number, number]): void {
|
||||
const wordPosition = this._getWordAt(coords, true);
|
||||
if (wordPosition) {
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : (wordPosition.start + wordPosition.length), coords[1]];
|
||||
let endRow = coords[1];
|
||||
|
||||
// Adjust negative start value
|
||||
while (wordPosition.start < 0) {
|
||||
wordPosition.start += this._terminal.cols;
|
||||
endRow--;
|
||||
}
|
||||
|
||||
// Adjust wrapped length value, this only needs to happen when values are reversed as in that
|
||||
// case we're interested in the start of the word, not the end
|
||||
if (!this._model.areSelectionValuesReversed()) {
|
||||
while (wordPosition.start + wordPosition.length > this._terminal.cols) {
|
||||
wordPosition.length -= this._terminal.cols;
|
||||
endRow++;
|
||||
}
|
||||
}
|
||||
|
||||
this._model.selectionEnd = [this._model.areSelectionValuesReversed() ? wordPosition.start : wordPosition.start + wordPosition.length, endRow];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -789,7 +902,9 @@ export class SelectionManager extends EventEmitter implements ISelectionManager
|
||||
* @param line The line index.
|
||||
*/
|
||||
protected _selectLineAt(line: number): void {
|
||||
this._model.selectionStart = [0, line];
|
||||
this._model.selectionStartLength = this._terminal.cols;
|
||||
const wrappedRange = this._buffer.getWrappedRangeForLine(line);
|
||||
this._model.selectionStart = [0, wrappedRange.first];
|
||||
this._model.selectionEnd = [this._terminal.cols, wrappedRange.last];
|
||||
this._model.selectionStartLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ class TestSelectionModel extends SelectionModel {
|
||||
}
|
||||
|
||||
describe('SelectionManager', () => {
|
||||
let window: Window;
|
||||
let document: Document;
|
||||
|
||||
let terminal: ITerminal;
|
||||
let model: TestSelectionModel;
|
||||
|
||||
@@ -131,6 +128,11 @@ describe('SelectionManager', () => {
|
||||
model.selectionEnd = [3, 2];
|
||||
assert.deepEqual(model.finalSelectionEnd, [4, 2]);
|
||||
});
|
||||
it('should return the end on a different row when start + length overflows onto a following row', () => {
|
||||
model.selectionStart = [78, 2];
|
||||
model.selectionStartLength = 4;
|
||||
assert.deepEqual(model.finalSelectionEnd, [2, 3]);
|
||||
});
|
||||
it('should return selection end if selection end is after selection start + length', () => {
|
||||
model.selectionStart = [2, 2];
|
||||
model.selectionStartLength = 2;
|
||||
|
||||
@@ -76,9 +76,13 @@ export class SelectionModel {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the selection start if the end doesn't exist or they're reversed
|
||||
// Use the selection start + length if the end doesn't exist or they're reversed
|
||||
if (!this.selectionEnd || this.areSelectionValuesReversed()) {
|
||||
return [this.selectionStart[0] + this.selectionStartLength, this.selectionStart[1]];
|
||||
const startPlusLength = this.selectionStart[0] + this.selectionStartLength;
|
||||
if (startPlusLength > this._terminal.cols) {
|
||||
return [startPlusLength % this._terminal.cols, this.selectionStart[1] + Math.floor(startPlusLength / this._terminal.cols)];
|
||||
}
|
||||
return [startPlusLength, this.selectionStart[1]];
|
||||
}
|
||||
|
||||
// Ensure the the word/line is selected after a double/triple click
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISoundManager } from './Types';
|
||||
|
||||
// Source: https://freesound.org/people/altemark/sounds/45759/
|
||||
// This sound is released under the Creative Commons Attribution 3.0 Unported
|
||||
// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been
|
||||
// made, apart from the conversion to base64.
|
||||
export const DEFAULT_BELL_SOUND = 'data:audio/wav;base64,UklGRigBAABXQVZFZm10IBAAAAABAAEARKwAAIhYAQACABAAZGF0YQQBAADpAFgCwAMlBZoG/wdmCcoKRAypDQ8PbRDBEQQTOxRtFYcWlBePGIUZXhoiG88bcBz7HHIdzh0WHlMeZx51HmkeUx4WHs8dah0AHXwc3hs9G4saxRnyGBIYGBcQFv8U4RPAEoYRQBACD70NWwwHC6gJOwjWBloF7gOBAhABkf8b/qv8R/ve+Xf4Ife79W/0JfPZ8Z/wde9N7ijtE+wU6xvqM+lb6H7nw+YX5mrlxuQz5Mzje+Ma49fioeKD4nXiYeJy4pHitOL04j/jn+MN5IPkFOWs5U3mDefM55/ogOl36m7rdOyE7abuyu8D8Unyj/Pg9D/2qfcb+Yn6/vuK/Qj/lAAlAg==';
|
||||
|
||||
export class SoundManager implements ISoundManager {
|
||||
private _audioContext: AudioContext;
|
||||
|
||||
constructor(
|
||||
private _terminal: ITerminal
|
||||
) {
|
||||
}
|
||||
|
||||
public playBellSound(): void {
|
||||
const audioContextCtor: typeof AudioContext = (<any>window).AudioContext || (<any>window).webkitAudioContext;
|
||||
if (!this._audioContext && audioContextCtor) {
|
||||
this._audioContext = new audioContextCtor();
|
||||
}
|
||||
|
||||
if (this._audioContext) {
|
||||
const bellAudioSource = this._audioContext.createBufferSource();
|
||||
const context = this._audioContext;
|
||||
this._audioContext.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._terminal.options.bellSound)), (buffer) => {
|
||||
bellAudioSource.buffer = buffer;
|
||||
bellAudioSource.connect(context.destination);
|
||||
bellAudioSource.start(0);
|
||||
});
|
||||
} else {
|
||||
console.warn('Sorry, but the Web Audio API is not supported by your browser. Please, consider upgrading to the latest version');
|
||||
}
|
||||
}
|
||||
|
||||
private _base64ToArrayBuffer(base64: string): ArrayBuffer {
|
||||
const binaryString = window.atob(base64);
|
||||
const len = binaryString.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
}
|
||||
|
||||
return bytes.buffer;
|
||||
}
|
||||
|
||||
private _removeMimeType(dataURI: string): string {
|
||||
// Split the input to get the mime-type and the data itself
|
||||
const splitUri = dataURI.split(',');
|
||||
|
||||
// Return only the data
|
||||
return splitUri[1];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export let blankLine = 'Blank line';
|
||||
export let promptLabel = 'Terminal input';
|
||||
export let tooMuchOutput = 'Too much output to announce, navigate to rows manually to read';
|
||||
+26
-21
@@ -14,6 +14,11 @@ import * as pty from 'node-pty';
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import { CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { IViewport } from './Types';
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
innerWrite(): void { this._innerWrite(); }
|
||||
}
|
||||
|
||||
let primitivePty: any;
|
||||
|
||||
@@ -24,8 +29,8 @@ let primitivePty: any;
|
||||
function ptyWriteRead(data: string, cb: (result: string) => void): void {
|
||||
fs.writeSync(primitivePty.slave, data);
|
||||
setTimeout(() => {
|
||||
let b = new Buffer(64000);
|
||||
let bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
|
||||
const b = new Buffer(64000);
|
||||
const bytes = fs.readSync(primitivePty.master, b, 0, 64000, null);
|
||||
cb(b.toString('utf8', 0, bytes));
|
||||
});
|
||||
}
|
||||
@@ -45,7 +50,7 @@ function formatError(input: string, output: string, expected: string): string {
|
||||
return '\x1b[33m' + (' ' + counter).slice(-2) + color + s;
|
||||
};
|
||||
}
|
||||
let line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
|
||||
const line80 = '12345678901234567890123456789012345678901234567890123456789012345678901234567890';
|
||||
let s = '';
|
||||
s += '\n\x1b[34m' + JSON.stringify(input);
|
||||
s += '\n\x1b[33m ' + line80 + '\n';
|
||||
@@ -62,7 +67,7 @@ function terminalToString(term: Terminal): string {
|
||||
for (let line = term.buffer.ybase; line < term.buffer.ybase + term.rows; line++) {
|
||||
lineText = '';
|
||||
for (let cell = 0; cell < term.cols; ++cell) {
|
||||
lineText += term.buffer.lines.get(line)[cell][CHAR_DATA_CHAR_INDEX];
|
||||
lineText += term.buffer.lines.get(line).get(cell)[CHAR_DATA_CHAR_INDEX];
|
||||
}
|
||||
// rtrim empty cells as xterm does
|
||||
lineText = lineText.replace(/\s+$/, '');
|
||||
@@ -74,34 +79,35 @@ function terminalToString(term: Terminal): string {
|
||||
|
||||
// Skip tests on Windows since pty.open isn't supported
|
||||
if (os.platform() !== 'win32') {
|
||||
let CONSOLE_LOG = console.log;
|
||||
const consoleLog = console.log;
|
||||
|
||||
// expect files need terminal at 80x25!
|
||||
let COLS = 80;
|
||||
let ROWS = 25;
|
||||
const cols = 80;
|
||||
const rows = 25;
|
||||
|
||||
/** some helpers for pty interaction */
|
||||
// we need a pty in between to get the termios decorations
|
||||
// for the basic test cases a raw pty device is enough
|
||||
primitivePty = pty.native.open(COLS, ROWS);
|
||||
primitivePty = (<any>pty).native.open(cols, rows);
|
||||
|
||||
/** tests */
|
||||
describe('xterm output comparison', () => {
|
||||
let xterm;
|
||||
let xterm: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
xterm = new Terminal({ cols: COLS, rows: ROWS });
|
||||
xterm = new TestTerminal({ cols: cols, rows: rows });
|
||||
xterm.refresh = () => {};
|
||||
xterm.viewport = {
|
||||
xterm.viewport = <IViewport>{
|
||||
syncScrollArea: () => {}
|
||||
};
|
||||
});
|
||||
|
||||
// omit stack trace for escape sequence files
|
||||
Error.stackTraceLimit = 0;
|
||||
let files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
||||
const files = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__dirname, '..')});
|
||||
// for (let i = 0; i < files.length; ++i) console.debug(i, files[i]);
|
||||
// only successful tests for now
|
||||
let skip = [
|
||||
const skip = [
|
||||
10, 16, 17, 19, 32, 33, 34, 35, 36, 39,
|
||||
40, 42, 43, 44, 45, 46, 47, 48, 49, 50,
|
||||
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
|
||||
@@ -118,7 +124,7 @@ if (os.platform() !== 'win32') {
|
||||
((filename: string) => {
|
||||
it(filename.split('/').slice(-1)[0], done => {
|
||||
ptyReset(() => {
|
||||
let inFile = fs.readFileSync(filename, 'utf8');
|
||||
const inFile = fs.readFileSync(filename, 'utf8');
|
||||
ptyWriteRead(inFile, fromPty => {
|
||||
// uncomment this to get log from terminal
|
||||
// console.log = function(){};
|
||||
@@ -127,12 +133,13 @@ if (os.platform() !== 'win32') {
|
||||
xterm.writeBuffer.push(fromPty);
|
||||
xterm.innerWrite();
|
||||
|
||||
let fromEmulator = terminalToString(xterm);
|
||||
console.log = CONSOLE_LOG;
|
||||
let expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
const fromEmulator = terminalToString(xterm);
|
||||
console.log = consoleLog;
|
||||
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
|
||||
// Some of the tests have whitespace on the right of lines, we trim all the linex
|
||||
// from xterm.js so ignore this for now at least.
|
||||
let expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
|
||||
const expectedRightTrimmed = expected.split('\n').map(l => l.replace(/\s+$/, '')).join('\n');
|
||||
if (fromEmulator !== expectedRightTrimmed) {
|
||||
// uncomment to get noisy output
|
||||
throw new Error(formatError(inFile, fromEmulator, expected));
|
||||
@@ -155,9 +162,7 @@ describe('typings', () => {
|
||||
tsc += '.cmd';
|
||||
}
|
||||
const fixtureDir = path.join(__dirname, '..', 'fixtures', 'typings-test');
|
||||
let result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
||||
const result = cp.spawnSync(tsc, { cwd: fixtureDir });
|
||||
assert.equal(result.status, 0, `build did not succeed:\nstdout: ${result.stdout.toString()}\nstderr: ${result.stderr.toString()}\n`);
|
||||
// Clean up
|
||||
fs.unlinkSync(path.join(fixtureDir, 'typings-test.js'));
|
||||
});
|
||||
});
|
||||
|
||||
+204
-316
@@ -5,35 +5,35 @@
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import * as attach from './addons/attach/attach';
|
||||
import { MockViewport, MockCompositionHelper, MockRenderer } from './utils/TestUtils.test';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from './Buffer';
|
||||
import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, DEFAULT_ATTR } from './Buffer';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
|
||||
class TestTerminal extends Terminal {
|
||||
public evaluateKeyEscapeSequence(ev: any): {cancel: boolean, key: string, scrollLines: number} { return this._evaluateKeyEscapeSequence(<KeyboardEvent>ev); }
|
||||
public keyDown(ev: any): boolean { return this._keyDown(ev); }
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('term.js addons', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
term = new TestTerminal({
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
});
|
||||
term = new TestTerminal(termOptions);
|
||||
term.refresh = () => {};
|
||||
(<any>term).renderer = new MockRenderer();
|
||||
term.viewport = new MockViewport();
|
||||
(<any>term).compositionHelper = new MockCompositionHelper();
|
||||
(<any>term)._compositionHelper = new MockCompositionHelper();
|
||||
// Force synchronous writes
|
||||
term.write = (data) => {
|
||||
term.writeBuffer.push(data);
|
||||
(<any>term).innerWrite();
|
||||
(<any>term)._innerWrite();
|
||||
};
|
||||
(<any>term).element = {
|
||||
classList: {
|
||||
@@ -43,11 +43,13 @@ describe('term.js addons', () => {
|
||||
};
|
||||
});
|
||||
|
||||
it('should apply addons with Terminal.applyAddon', () => {
|
||||
Terminal.applyAddon(attach);
|
||||
// Test that addon was applied successfully, adding attach to Terminal's
|
||||
// prototype.
|
||||
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
|
||||
it('should not mutate the options parameter', () => {
|
||||
term.setOption('cols', 1000);
|
||||
|
||||
assert.deepEqual(termOptions, {
|
||||
cols: INIT_COLS,
|
||||
rows: INIT_ROWS
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOption', () => {
|
||||
@@ -67,13 +69,13 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
describe('attachCustomKeyEventHandler', () => {
|
||||
let evKeyDown = <KeyboardEvent>{
|
||||
const evKeyDown = <KeyboardEvent>{
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
type: 'keydown',
|
||||
keyCode: 77
|
||||
};
|
||||
let evKeyPress = <KeyboardEvent>{
|
||||
const evKeyPress = <KeyboardEvent>{
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {},
|
||||
type: 'keypress',
|
||||
@@ -119,9 +121,20 @@ describe('term.js addons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset', () => {
|
||||
it('should not affect cursorState', () => {
|
||||
term.cursorState = 1;
|
||||
term.reset();
|
||||
assert.equal(term.cursorState, 1);
|
||||
term.cursorState = 0;
|
||||
term.reset();
|
||||
assert.equal(term.cursorState, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clear', () => {
|
||||
it('should clear a buffer equal to rows', () => {
|
||||
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
term.clear();
|
||||
assert.equal(term.buffer.y, 0);
|
||||
assert.equal(term.buffer.ybase, 0);
|
||||
@@ -129,7 +142,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
||||
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
it('should clear a buffer larger than rows', () => {
|
||||
@@ -138,7 +151,7 @@ describe('term.js addons', () => {
|
||||
term.write('test\n');
|
||||
}
|
||||
|
||||
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
term.clear();
|
||||
assert.equal(term.buffer.y, 0);
|
||||
assert.equal(term.buffer.ybase, 0);
|
||||
@@ -146,11 +159,11 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
||||
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
it('should not break the prompt when cleared twice', () => {
|
||||
let promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
|
||||
term.clear();
|
||||
term.clear();
|
||||
assert.equal(term.buffer.y, 0);
|
||||
@@ -159,14 +172,14 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.lines.length, term.rows);
|
||||
assert.deepEqual(term.buffer.lines.get(0), promptLine);
|
||||
for (let i = 1; i < term.rows; i++) {
|
||||
assert.deepEqual(term.buffer.lines.get(i), term.blankLine());
|
||||
assert.deepEqual(term.buffer.lines.get(i), BufferLine.blankLine(term.cols, DEFAULT_ATTR));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('scroll', () => {
|
||||
describe('scrollLines', () => {
|
||||
let startYDisp;
|
||||
let startYDisp: number;
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < term.rows * 2; i++) {
|
||||
term.writeln('test');
|
||||
@@ -201,7 +214,7 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
describe('scrollPages', () => {
|
||||
let startYDisp;
|
||||
let startYDisp: number;
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
term.writeln('test');
|
||||
@@ -238,7 +251,7 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
describe('scrollToBottom', () => {
|
||||
let startYDisp;
|
||||
let startYDisp: number;
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
term.writeln('test');
|
||||
@@ -258,22 +271,47 @@ describe('term.js addons', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyDown', () => {
|
||||
describe('scrollToLine', () => {
|
||||
let startYDisp: number;
|
||||
beforeEach(() => {
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
term.writeln('test');
|
||||
}
|
||||
startYDisp = (term.rows * 2) + 1;
|
||||
});
|
||||
it('should scroll to requested line', () => {
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(0);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
term.scrollToLine(10);
|
||||
assert.equal(term.buffer.ydisp, 10);
|
||||
term.scrollToLine(startYDisp);
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(20);
|
||||
assert.equal(term.buffer.ydisp, 20);
|
||||
});
|
||||
it('should not scroll beyond boundary lines', () => {
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollToLine(-1);
|
||||
assert.equal(term.buffer.ydisp, 0);
|
||||
term.scrollToLine(startYDisp + 1);
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('keyPress', () => {
|
||||
it('should scroll down, when a key is pressed and terminal is scrolled up', () => {
|
||||
// Override _evaluateKeyEscapeSequence to return cancel code
|
||||
(<any>term)._evaluateKeyEscapeSequence = () => {
|
||||
return { key: 'a' };
|
||||
};
|
||||
let event = <KeyboardEvent>{
|
||||
const event = <KeyboardEvent>{
|
||||
type: 'keydown',
|
||||
keyCode: 0,
|
||||
key: 'a',
|
||||
keyCode: 65,
|
||||
preventDefault: () => {},
|
||||
stopPropagation: () => {}
|
||||
};
|
||||
|
||||
term.buffer.ydisp = 0;
|
||||
term.buffer.ybase = 40;
|
||||
term.keyDown(event);
|
||||
term.keyPress(event);
|
||||
|
||||
// Ensure that now the terminal is scrolled to bottom
|
||||
assert.equal(term.buffer.ydisp, term.buffer.ybase);
|
||||
@@ -284,7 +322,7 @@ describe('term.js addons', () => {
|
||||
for (let i = 0; i < term.rows * 3; i++) {
|
||||
term.writeln('test');
|
||||
}
|
||||
let startYDisp = (term.rows * 2) + 1;
|
||||
const startYDisp = (term.rows * 2) + 1;
|
||||
term.attachCustomKeyEventHandler(() => {
|
||||
return false;
|
||||
});
|
||||
@@ -292,7 +330,7 @@ describe('term.js addons', () => {
|
||||
assert.equal(term.buffer.ydisp, startYDisp);
|
||||
term.scrollLines(-1);
|
||||
assert.equal(term.buffer.ydisp, startYDisp - 1);
|
||||
term.keyDown(<KeyboardEvent>{ keyCode: 0 });
|
||||
term.keyPress(<KeyboardEvent>{ keyCode: 0 });
|
||||
assert.equal(term.buffer.ydisp, startYDisp - 1);
|
||||
});
|
||||
});
|
||||
@@ -300,62 +338,62 @@ describe('term.js addons', () => {
|
||||
describe('scroll() function', () => {
|
||||
describe('when scrollback > 0', () => {
|
||||
it('should create a new line and scroll', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS + 1);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a', '\'a\' should be pushed to the scrollback');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(5).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -366,223 +404,73 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
it('should create a new line and shift everything up', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
// 'a' gets pushed out of buffer
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1)[0][CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 2).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(INIT_ROWS - 1).get(0)[CHAR_DATA_CHAR_INDEX], ' ');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = 3;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'b');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
|
||||
it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => {
|
||||
term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX] = 'a';
|
||||
term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX] = 'b';
|
||||
term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX] = 'c';
|
||||
term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX] = 'd';
|
||||
term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX] = 'e';
|
||||
term.buffer.y = INIT_ROWS - 1; // Move cursor to last line
|
||||
term.buffer.scrollTop = 1;
|
||||
term.buffer.scrollBottom = 3;
|
||||
term.scroll();
|
||||
assert.equal(term.buffer.lines.length, INIT_ROWS);
|
||||
assert.equal(term.buffer.lines.get(0)[0][CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2)[0][CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3)[0][CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4)[0][CHAR_DATA_CHAR_INDEX], 'e');
|
||||
assert.equal(term.buffer.lines.get(0).get(0)[CHAR_DATA_CHAR_INDEX], 'a');
|
||||
assert.equal(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX], 'c', '\'b\' should be removed from the buffer');
|
||||
assert.equal(term.buffer.lines.get(2).get(0)[CHAR_DATA_CHAR_INDEX], 'd');
|
||||
assert.equal(term.buffer.lines.get(3).get(0)[CHAR_DATA_CHAR_INDEX], ' ', 'a blank line should be added at scrollBottom\'s index');
|
||||
assert.equal(term.buffer.lines.get(4).get(0)[CHAR_DATA_CHAR_INDEX], 'e');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('evaluateKeyEscapeSequence', () => {
|
||||
it('should return the correct escape sequence for unmodified keys', () => {
|
||||
// Backspace
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 8 }).key, '\x7f'); // ^?
|
||||
// Tab
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 9 }).key, '\t');
|
||||
// Return/enter
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 13 }).key, '\r'); // CR
|
||||
// Escape
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 27 }).key, '\x1b');
|
||||
// Page up, page down
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
|
||||
// End, Home
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
|
||||
// Left, up, right, down arrows
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 37 }).key, '\x1b[D'); // CSI D
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 38 }).key, '\x1b[A'); // CSI A
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 39 }).key, '\x1b[C'); // CSI C
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 40 }).key, '\x1b[B'); // CSI B
|
||||
// Insert
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
|
||||
// Delete
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
|
||||
// F1-F12
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
|
||||
});
|
||||
it('should return \\x1b[3;5~ for ctrl+delete', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
|
||||
});
|
||||
it('should return \\x1b[3;2~ for shift+delete', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
|
||||
});
|
||||
it('should return \\x1b[3;3~ for alt+delete', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
|
||||
});
|
||||
it('should return \\x1b[5D for ctrl+left', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1b[5C for ctrl+right', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
|
||||
});
|
||||
it('should return \\x1b[5A for ctrl+up', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
||||
});
|
||||
it('should return \\x1b[5B for ctrl+down', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
||||
});
|
||||
|
||||
describe('On non-macOS platforms', () => {
|
||||
beforeEach(() => {
|
||||
term.browser.isMac = false;
|
||||
});
|
||||
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
it('should return \\x1b[5D for alt+left', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1b[5C for alt+right', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
|
||||
});
|
||||
it('should return \\x1ba for alt+a', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
|
||||
});
|
||||
});
|
||||
|
||||
describe('On macOS platforms', () => {
|
||||
beforeEach(() => {
|
||||
term.browser.isMac = true;
|
||||
});
|
||||
it('should return \\x1bb for alt+left', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 37 }).key, '\x1bb'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1bf for alt+right', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 39 }).key, '\x1bf'); // CSI 5 C
|
||||
});
|
||||
it('should return undefined for alt+a', () => {
|
||||
assert.strictEqual(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with macOptionIsMeta', () => {
|
||||
beforeEach(() => {
|
||||
term.browser.isMac = true;
|
||||
term.setOption('macOptionIsMeta', true);
|
||||
});
|
||||
it('should return \\x1ba for alt+a', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 65 }).key, '\x1ba');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return \\x1b[5A for alt+up', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
||||
});
|
||||
it('should return \\x1b[5B for alt+down', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
||||
});
|
||||
it('should return the correct escape sequence for modified F1-F12 keys', () => {
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
|
||||
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
|
||||
assert.equal(term.evaluateKeyEscapeSequence({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Third level shift', () => {
|
||||
let evKeyDown;
|
||||
let evKeyPress;
|
||||
let evKeyDown: any;
|
||||
let evKeyPress: any;
|
||||
|
||||
beforeEach(() => {
|
||||
term.handler = () => {};
|
||||
@@ -653,11 +541,11 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
it('should emit key with alt + key on keyPress', (done) => {
|
||||
let keys = ['@', '@', '\\', '\\', '|', '|'];
|
||||
const keys = ['@', '@', '\\', '\\', '|', '|'];
|
||||
|
||||
term.on('keypress', (key) => {
|
||||
if (key) {
|
||||
let index = keys.indexOf(key);
|
||||
const index = keys.indexOf(key);
|
||||
assert(index !== -1, 'Emitted wrong key: ' + key);
|
||||
keys.splice(index, 1);
|
||||
}
|
||||
@@ -719,11 +607,11 @@ describe('term.js addons', () => {
|
||||
});
|
||||
|
||||
it('should emit key with alt + ctrl + key on keyPress', (done) => {
|
||||
let keys = ['@', '@', '\\', '\\', '|', '|'];
|
||||
const keys = ['@', '@', '\\', '\\', '|', '|'];
|
||||
|
||||
term.on('keypress', (key) => {
|
||||
if (key) {
|
||||
let index = keys.indexOf(key);
|
||||
const index = keys.indexOf(key);
|
||||
assert(index !== -1, 'Emitted wrong key: ' + key);
|
||||
keys.splice(index, 1);
|
||||
}
|
||||
@@ -764,64 +652,64 @@ describe('term.js addons', () => {
|
||||
describe('unicode - surrogates', () => {
|
||||
it('2 characters per cell', function (): void {
|
||||
this.timeout(10000); // This is needed because istanbul patches code and slows it down
|
||||
let high = String.fromCharCode(0xD800);
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high + String.fromCharCode(i));
|
||||
let tchar = term.buffer.lines.get(0)[0];
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters at last cell', () => {
|
||||
let high = String.fromCharCode(0xD800);
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.write(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0)[term.buffer.x - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0).get(term.buffer.x - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end with autowrap', () => {
|
||||
let high = String.fromCharCode(0xD800);
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = true;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1)[0][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(term.buffer.lines.get(1).get(0)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('2 characters per cell over line end without autowrap', () => {
|
||||
let high = String.fromCharCode(0xD800);
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.buffer.x = term.cols - 1;
|
||||
term.wraparoundMode = false;
|
||||
term.write('a' + high + String.fromCharCode(i));
|
||||
// auto wraparound mode should cut off the rest of the line
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(1).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
it('splitted surrogates', () => {
|
||||
let high = String.fromCharCode(0xD800);
|
||||
const high = String.fromCharCode(0xD800);
|
||||
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
|
||||
term.write(high);
|
||||
term.write(String.fromCharCode(i));
|
||||
let tchar = term.buffer.lines.get(0)[0];
|
||||
const tchar = term.buffer.lines.get(0).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(high + String.fromCharCode(i));
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
term.reset();
|
||||
}
|
||||
});
|
||||
@@ -830,30 +718,30 @@ describe('term.js addons', () => {
|
||||
describe('unicode - combining characters', () => {
|
||||
it('café', () => {
|
||||
term.write('cafe\u0301');
|
||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0)[3][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(3)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('café - end of line', () => {
|
||||
term.buffer.x = term.cols - 1 - 3;
|
||||
term.write('cafe\u0301');
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0)[term.cols - 1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0)[1][CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(term.buffer.lines.get(0).get(term.cols - 1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(term.buffer.lines.get(0).get(1)[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
});
|
||||
it('multiple combined é', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('e\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
}
|
||||
let tchar = term.buffer.lines.get(1)[0];
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('e\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
@@ -862,12 +750,12 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(100).join('\uD800\uDC00\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
}
|
||||
let tchar = term.buffer.lines.get(1)[0];
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\uD800\uDC00\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
@@ -890,7 +778,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -901,7 +789,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(1)[0];
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -911,7 +799,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -922,11 +810,11 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1)[0];
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -936,7 +824,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -947,11 +835,11 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1)[0];
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -960,7 +848,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('¥\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -971,7 +859,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(1)[0];
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('¥\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(2);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -981,7 +869,7 @@ describe('term.js addons', () => {
|
||||
term.buffer.x = 1;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 1; i < term.cols - 1; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (!(i % 2)) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -992,11 +880,11 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(0)[term.cols - 1];
|
||||
let tchar = term.buffer.lines.get(0).get(term.cols - 1);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql(' ');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(1);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(1);
|
||||
tchar = term.buffer.lines.get(1)[0];
|
||||
tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -1005,7 +893,7 @@ describe('term.js addons', () => {
|
||||
term.wraparoundMode = true;
|
||||
term.write(Array(50).join('\ud843\ude6d\u0301'));
|
||||
for (let i = 0; i < term.cols; ++i) {
|
||||
let tchar = term.buffer.lines.get(0)[i];
|
||||
const tchar = term.buffer.lines.get(0).get(i);
|
||||
if (i % 2) {
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(0);
|
||||
@@ -1016,7 +904,7 @@ describe('term.js addons', () => {
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
}
|
||||
}
|
||||
let tchar = term.buffer.lines.get(1)[0];
|
||||
const tchar = term.buffer.lines.get(1).get(0);
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX]).eql('\ud843\ude6d\u0301');
|
||||
expect(tchar[CHAR_DATA_CHAR_INDEX].length).eql(3);
|
||||
expect(tchar[CHAR_DATA_WIDTH_INDEX]).eql(2);
|
||||
@@ -1031,10 +919,10 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('abcde');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('e');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('0');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('4');
|
||||
});
|
||||
it('fullwidth - insert', () => {
|
||||
term.write(Array(9).join('0123456789').slice(-80));
|
||||
@@ -1043,11 +931,11 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('¥¥¥');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0)[14][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0)[15][CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(14)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(15)[CHAR_DATA_CHAR_INDEX]).eql('');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql('3');
|
||||
});
|
||||
it('fullwidth - right border', () => {
|
||||
term.write(Array(41).join('¥'));
|
||||
@@ -1056,14 +944,14 @@ describe('term.js addons', () => {
|
||||
term.insertMode = true;
|
||||
term.write('a');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0)[10][CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
expect(term.buffer.lines.get(0).get(10)[CHAR_DATA_CHAR_INDEX]).eql('a');
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(' '); // fullwidth char got replaced
|
||||
term.write('b');
|
||||
expect(term.buffer.lines.get(0).length).eql(term.cols);
|
||||
expect(term.buffer.lines.get(0)[11][CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
expect(term.buffer.lines.get(0)[12][CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0)[79][CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||
expect(term.buffer.lines.get(0).get(11)[CHAR_DATA_CHAR_INDEX]).eql('b');
|
||||
expect(term.buffer.lines.get(0).get(12)[CHAR_DATA_CHAR_INDEX]).eql('¥');
|
||||
expect(term.buffer.lines.get(0).get(79)[CHAR_DATA_CHAR_INDEX]).eql(''); // empty cell after fullwidth
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+543
-888
File diff suppressed because it is too large
Load Diff
+216
-41
@@ -3,19 +3,24 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter } from 'xterm';
|
||||
import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ICharset } from './core/Types';
|
||||
|
||||
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
|
||||
|
||||
export type XtermListener = (...args: any[]) => void;
|
||||
|
||||
export type CharData = [number, string, number, number];
|
||||
export type LineData = CharData[];
|
||||
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
|
||||
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
export enum LinkHoverEventTypes {
|
||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
|
||||
export const enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
LEAVE = 'linkleave'
|
||||
@@ -41,9 +46,8 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
insertMode: boolean;
|
||||
wraparoundMode: boolean;
|
||||
bracketedPasteMode: boolean;
|
||||
defAttr: number;
|
||||
curAttr: number;
|
||||
prefix: string;
|
||||
savedCurAttr: number;
|
||||
savedCols: number;
|
||||
x10Mouse: boolean;
|
||||
vt200Mouse: boolean;
|
||||
@@ -62,17 +66,11 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
|
||||
bell(): void;
|
||||
focus(): void;
|
||||
convertEol: boolean;
|
||||
updateRange(y: number): void;
|
||||
scroll(isWrapped?: boolean): void;
|
||||
setgLevel(g: number): void;
|
||||
eraseAttr(): number;
|
||||
eraseRight(x: number, y: number): void;
|
||||
eraseLine(y: number): void;
|
||||
eraseLeft(x: number, y: number): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean): LineData;
|
||||
is(term: string): boolean;
|
||||
send(data: string): void;
|
||||
setgCharset(g: number, charset: ICharset): void;
|
||||
resize(x: number, y: number): void;
|
||||
log(text: string, data?: any): void;
|
||||
@@ -82,11 +80,17 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
matchColor(r1: number, g1: number, b1: number): number;
|
||||
error(text: string, data?: any): void;
|
||||
setOption(key: string, value: any): void;
|
||||
tabSet(): void;
|
||||
handler(data: string): void;
|
||||
handleTitle(title: string): void;
|
||||
index(): void;
|
||||
reverseIndex(): void;
|
||||
}
|
||||
|
||||
export interface IViewport {
|
||||
export interface IViewport extends IDisposable {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(): void;
|
||||
getLinesScrolled(ev: WheelEvent): number;
|
||||
onWheel(ev: WheelEvent): void;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): void;
|
||||
@@ -102,10 +106,11 @@ export interface ICompositionHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles actions generated by the parser.
|
||||
* Calls the parser and handles actions generated by the parser.
|
||||
*/
|
||||
export interface IInputHandler {
|
||||
addChar(char: string, code: number): void;
|
||||
parse(data: string): void;
|
||||
print(data: string, start: number, end: number): void;
|
||||
|
||||
/** C0 BEL */ bell(): void;
|
||||
/** C0 LF */ lineFeed(): void;
|
||||
@@ -131,26 +136,49 @@ export interface IInputHandler {
|
||||
/** CSI M */ deleteLines(params?: number[]): void;
|
||||
/** CSI P */ deleteChars(params?: number[]): void;
|
||||
/** CSI S */ scrollUp(params?: number[]): void;
|
||||
/** CSI T */ scrollDown(params?: number[]): void;
|
||||
/** CSI T */ scrollDown(params?: number[], collect?: string): void;
|
||||
/** CSI X */ eraseChars(params?: number[]): void;
|
||||
/** CSI Z */ cursorBackwardTab(params?: number[]): void;
|
||||
/** CSI ` */ charPosAbsolute(params?: number[]): void;
|
||||
/** CSI a */ HPositionRelative(params?: number[]): void;
|
||||
/** CSI a */ hPositionRelative(params?: number[]): void;
|
||||
/** CSI b */ repeatPrecedingCharacter(params?: number[]): void;
|
||||
/** CSI c */ sendDeviceAttributes(params?: number[]): void;
|
||||
/** CSI c */ sendDeviceAttributes(params?: number[], collect?: string): void;
|
||||
/** CSI d */ linePosAbsolute(params?: number[]): void;
|
||||
/** CSI e */ VPositionRelative(params?: number[]): void;
|
||||
/** CSI f */ HVPosition(params?: number[]): void;
|
||||
/** CSI e */ vPositionRelative(params?: number[]): void;
|
||||
/** CSI f */ hVPosition(params?: number[]): void;
|
||||
/** CSI g */ tabClear(params?: number[]): void;
|
||||
/** CSI h */ setMode(params?: number[]): void;
|
||||
/** CSI l */ resetMode(params?: number[]): void;
|
||||
/** CSI h */ setMode(params?: number[], collect?: string): void;
|
||||
/** CSI l */ resetMode(params?: number[], collect?: string): void;
|
||||
/** CSI m */ charAttributes(params?: number[]): void;
|
||||
/** CSI n */ deviceStatus(params?: number[]): void;
|
||||
/** CSI p */ softReset(params?: number[]): void;
|
||||
/** CSI q */ setCursorStyle(params?: number[]): void;
|
||||
/** CSI r */ setScrollRegion(params?: number[]): void;
|
||||
/** CSI n */ deviceStatus(params?: number[], collect?: string): void;
|
||||
/** CSI p */ softReset(params?: number[], collect?: string): void;
|
||||
/** CSI q */ setCursorStyle(params?: number[], collect?: string): void;
|
||||
/** CSI r */ setScrollRegion(params?: number[], collect?: string): void;
|
||||
/** CSI s */ saveCursor(params?: number[]): void;
|
||||
/** CSI u */ restoreCursor(params?: number[]): void;
|
||||
/** OSC 0
|
||||
OSC 2 */ setTitle(data: string): void;
|
||||
/** ESC E */ nextLine(): void;
|
||||
/** ESC = */ keypadApplicationMode(): void;
|
||||
/** ESC > */ keypadNumericMode(): void;
|
||||
/** ESC % G
|
||||
ESC % @ */ selectDefaultCharset(): void;
|
||||
/** ESC ( C
|
||||
ESC ) C
|
||||
ESC * C
|
||||
ESC + C
|
||||
ESC - C
|
||||
ESC . C
|
||||
ESC / C */ selectCharset(collectAndFlag: string): void;
|
||||
/** ESC D */ index(): void;
|
||||
/** ESC H */ tabSet(): void;
|
||||
/** ESC M */ reverseIndex(): void;
|
||||
/** ESC c */ reset(): void;
|
||||
/** ESC n
|
||||
ESC o
|
||||
ESC |
|
||||
ESC }
|
||||
ESC ~ */ setgLevel(level: number): void;
|
||||
}
|
||||
|
||||
export interface ILinkMatcher {
|
||||
@@ -165,14 +193,13 @@ export interface ILinkMatcher {
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ICharset {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface ILinkHoverEvent {
|
||||
x: number;
|
||||
y: number;
|
||||
length: number;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number;
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
@@ -184,7 +211,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
writeBuffer: string[];
|
||||
cursorHidden: boolean;
|
||||
cursorState: number;
|
||||
defAttr: number;
|
||||
options: ITerminalOptions;
|
||||
buffer: IBuffer;
|
||||
buffers: IBufferSet;
|
||||
@@ -199,12 +225,10 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
* @param data The data to populate in the event.
|
||||
*/
|
||||
handler(data: string): void;
|
||||
send(data: string): void;
|
||||
scrollLines(disp: number, suppressScrollEvent?: boolean): void;
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
log(text: string): void;
|
||||
showCursor(): void;
|
||||
blankLine(cur?: boolean, isWrapped?: boolean, cols?: number): LineData;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
@@ -220,7 +244,7 @@ export interface ILinkifierAccessor {
|
||||
}
|
||||
|
||||
export interface IMouseHelper {
|
||||
getCoords(event: {pageX: number, pageY: number}, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getCoords(event: { pageX: number, pageY: number }, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number, isSelection?: boolean): [number, number];
|
||||
getRawByteCoords(event: MouseEvent, element: HTMLElement, charMeasure: ICharMeasure, lineHeight: number, colCount: number, rowCount: number): { x: number, y: number };
|
||||
}
|
||||
|
||||
@@ -232,6 +256,7 @@ export interface ICharMeasure {
|
||||
|
||||
// TODO: The options that are not in the public API should be reviewed
|
||||
export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
[key: string]: any;
|
||||
cancelEvents?: boolean;
|
||||
convertEol?: boolean;
|
||||
debug?: boolean;
|
||||
@@ -242,7 +267,7 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
}
|
||||
|
||||
export interface IBuffer {
|
||||
lines: ICircularList<LineData>;
|
||||
readonly lines: ICircularList<IBufferLine>;
|
||||
ydisp: number;
|
||||
ybase: number;
|
||||
y: number;
|
||||
@@ -250,10 +275,12 @@ export interface IBuffer {
|
||||
tabs: any;
|
||||
scrollBottom: number;
|
||||
scrollTop: number;
|
||||
hasScrollback: boolean;
|
||||
savedY: number;
|
||||
savedX: number;
|
||||
isCursorInViewport: boolean;
|
||||
translateBufferLineToString(lineIndex: number, trimRight: boolean, startCol?: number, endCol?: number): string;
|
||||
getWrappedRangeForLine(y: number): { first: number, last: number };
|
||||
nextStop(x?: number): number;
|
||||
prevStop(x?: number): number;
|
||||
}
|
||||
@@ -270,7 +297,6 @@ export interface IBufferSet extends IEventEmitter {
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
forEach: (callbackfn: (value: T, index: number) => void) => void;
|
||||
|
||||
get(index: number): T;
|
||||
set(index: number, value: T): void;
|
||||
@@ -296,8 +322,6 @@ export interface ISelectionManager {
|
||||
export interface ILinkifier extends IEventEmitter {
|
||||
attachToDom(mouseZoneManager: IMouseZoneManager): void;
|
||||
linkifyRows(start: number, end: number): void;
|
||||
setHypertextLinkHandler(handler: LinkMatcherHandler): void;
|
||||
setHypertextValidationCallback(callback: LinkMatcherValidationCallback): void;
|
||||
registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): boolean;
|
||||
}
|
||||
@@ -347,3 +371,154 @@ export interface IBrowser {
|
||||
isIphone: boolean;
|
||||
isMSWindows: boolean;
|
||||
}
|
||||
|
||||
export interface ISoundManager {
|
||||
playBellSound(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal states of EscapeSequenceParser.
|
||||
*/
|
||||
export const enum ParserState {
|
||||
GROUND = 0,
|
||||
ESCAPE = 1,
|
||||
ESCAPE_INTERMEDIATE = 2,
|
||||
CSI_ENTRY = 3,
|
||||
CSI_PARAM = 4,
|
||||
CSI_INTERMEDIATE = 5,
|
||||
CSI_IGNORE = 6,
|
||||
SOS_PM_APC_STRING = 7,
|
||||
OSC_STRING = 8,
|
||||
DCS_ENTRY = 9,
|
||||
DCS_PARAM = 10,
|
||||
DCS_IGNORE = 11,
|
||||
DCS_INTERMEDIATE = 12,
|
||||
DCS_PASSTHROUGH = 13
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal actions of EscapeSequenceParser.
|
||||
*/
|
||||
export const enum ParserAction {
|
||||
IGNORE = 0,
|
||||
ERROR = 1,
|
||||
PRINT = 2,
|
||||
EXECUTE = 3,
|
||||
OSC_START = 4,
|
||||
OSC_PUT = 5,
|
||||
OSC_END = 6,
|
||||
CSI_DISPATCH = 7,
|
||||
PARAM = 8,
|
||||
COLLECT = 9,
|
||||
ESC_DISPATCH = 10,
|
||||
CLEAR = 11,
|
||||
DCS_HOOK = 12,
|
||||
DCS_PUT = 13,
|
||||
DCS_UNHOOK = 14
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal state of EscapeSequenceParser.
|
||||
* Used as argument of the error handler to allow
|
||||
* introspection at runtime on parse errors.
|
||||
* Return it with altered values to recover from
|
||||
* faulty states (not yet supported).
|
||||
* Set `abort` to `true` to abort the current parsing.
|
||||
*/
|
||||
export interface IParsingState {
|
||||
// position in parse string
|
||||
position: number;
|
||||
// actual character code
|
||||
code: number;
|
||||
// current parser state
|
||||
currentState: ParserState;
|
||||
// print buffer start index (-1 for not set)
|
||||
print: number;
|
||||
// dcs buffer start index (-1 for not set)
|
||||
dcs: number;
|
||||
// osc string buffer
|
||||
osc: string;
|
||||
// collect buffer with intermediate characters
|
||||
collect: string;
|
||||
// params buffer
|
||||
params: number[];
|
||||
// should abort (default: false)
|
||||
abort: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* DCS handler signature for EscapeSequenceParser.
|
||||
* EscapeSequenceParser handles DCS commands via separate
|
||||
* subparsers that get hook/unhooked and can handle
|
||||
* arbitrary amount of print data.
|
||||
* On entering a DSC sequence `hook` is called by
|
||||
* `EscapeSequenceParser`. Use it to initialize or reset
|
||||
* states needed to handle the current DCS sequence.
|
||||
* EscapeSequenceParser will call `put` several times if the
|
||||
* parsed string got splitted, therefore you might have to collect
|
||||
* `data` until `unhook` is called. `unhook` marks the end
|
||||
* of the current DCS sequence.
|
||||
*/
|
||||
export interface IDcsHandler {
|
||||
hook(collect: string, params: number[], flag: number): void;
|
||||
put(data: string, start: number, end: number): void;
|
||||
unhook(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* EscapeSequenceParser interface.
|
||||
*/
|
||||
export interface IEscapeSequenceParser extends IDisposable {
|
||||
/**
|
||||
* Reset the parser to its initial state (handlers are kept).
|
||||
*/
|
||||
reset(): void;
|
||||
|
||||
/**
|
||||
* Parse string `data`.
|
||||
* @param data The data to parse.
|
||||
*/
|
||||
parse(data: string): void;
|
||||
|
||||
setPrintHandler(callback: (data: string, start: number, end: number) => void): void;
|
||||
clearPrintHandler(): void;
|
||||
|
||||
setExecuteHandler(flag: string, callback: () => void): void;
|
||||
clearExecuteHandler(flag: string): void;
|
||||
setExecuteHandlerFallback(callback: (code: number) => void): void;
|
||||
|
||||
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
|
||||
clearCsiHandler(flag: string): void;
|
||||
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
|
||||
|
||||
setEscHandler(collectAndFlag: string, callback: () => void): void;
|
||||
clearEscHandler(collectAndFlag: string): void;
|
||||
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
|
||||
|
||||
setOscHandler(ident: number, callback: (data: string) => void): void;
|
||||
clearOscHandler(ident: number): void;
|
||||
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
|
||||
|
||||
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
|
||||
clearDcsHandler(collectAndFlag: string): void;
|
||||
setDcsHandlerFallback(handler: IDcsHandler): void;
|
||||
|
||||
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
|
||||
clearErrorHandler(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for a line in the terminal buffer.
|
||||
*/
|
||||
export interface IBufferLine {
|
||||
length: number;
|
||||
isWrapped: boolean;
|
||||
get(index: number): CharData;
|
||||
set(index: number, value: CharData): void;
|
||||
pop(): CharData | undefined;
|
||||
push(data: CharData): void;
|
||||
splice(start: number, deleteCount: number, ...items: CharData[]): CharData[];
|
||||
insertCells(pos: number, n: number, ch: CharData): void;
|
||||
deleteCells(pos: number, n: number, fill: CharData): void;
|
||||
replaceCells(start: number, end: number, fill: CharData): void;
|
||||
}
|
||||
|
||||
+121
-58
@@ -5,7 +5,9 @@
|
||||
|
||||
import { IColorSet } from './renderer/Types';
|
||||
import { ITerminal, IViewport } from './Types';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
@@ -13,80 +15,101 @@ const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
* Represents the viewport of a terminal, the visible area within the larger buffer of output.
|
||||
* Logic for the virtual scroll bar is included in this object.
|
||||
*/
|
||||
export class Viewport implements IViewport {
|
||||
export class Viewport extends Disposable implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private currentRowHeight: number = 0;
|
||||
private lastRecordedBufferLength: number = 0;
|
||||
private lastRecordedViewportHeight: number = 0;
|
||||
private lastRecordedBufferHeight: number = 0;
|
||||
private lastTouchY: number;
|
||||
private _currentRowHeight: number = 0;
|
||||
private _lastRecordedBufferLength: number = 0;
|
||||
private _lastRecordedViewportHeight: number = 0;
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
private _lastTouchY: number;
|
||||
|
||||
// Stores a partial line amount when scrolling, this is used to keep track of how much of a line
|
||||
// is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a
|
||||
// quick fix and could have a more robust solution in place that reset the value when needed.
|
||||
private _wheelPartialScroll: number = 0;
|
||||
|
||||
private _refreshAnimationFrame: number | null = null;
|
||||
private _ignoreNextScrollEvent: boolean = false;
|
||||
|
||||
/**
|
||||
* Creates a new Viewport.
|
||||
* @param terminal The terminal this viewport belongs to.
|
||||
* @param viewportElement The DOM element acting as the viewport.
|
||||
* @param scrollArea The DOM element acting as the scroll area.
|
||||
* @param charMeasure A DOM element used to measure the character size of. the terminal.
|
||||
* @param _terminal The terminal this viewport belongs to.
|
||||
* @param _viewportElement The DOM element acting as the viewport.
|
||||
* @param _scrollArea The DOM element acting as the scroll area.
|
||||
* @param _charMeasure A DOM element used to measure the character size of. the terminal.
|
||||
*/
|
||||
constructor(
|
||||
private terminal: ITerminal,
|
||||
private viewportElement: HTMLElement,
|
||||
private scrollArea: HTMLElement,
|
||||
private charMeasure: CharMeasure
|
||||
private _terminal: ITerminal,
|
||||
private _viewportElement: HTMLElement,
|
||||
private _scrollArea: HTMLElement,
|
||||
private _charMeasure: CharMeasure
|
||||
) {
|
||||
super();
|
||||
|
||||
// Measure the width of the scrollbar. If it is 0 we can assume it's an OSX overlay scrollbar.
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this.viewportElement.offsetWidth - this.scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this.viewportElement.addEventListener('scroll', this.onScroll.bind(this));
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
public onThemeChanged(colors: IColorSet): void {
|
||||
this.viewportElement.style.backgroundColor = colors.background;
|
||||
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes row height, setting line-height, viewport height and scroll area height if
|
||||
* necessary.
|
||||
*/
|
||||
private refresh(): void {
|
||||
if (this.charMeasure.height > 0) {
|
||||
this.currentRowHeight = this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this.lastRecordedViewportHeight = this.viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this.currentRowHeight * this.lastRecordedBufferLength) + (this.lastRecordedViewportHeight - this.terminal.renderer.dimensions.canvasHeight);
|
||||
if (this.lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this.lastRecordedBufferHeight = newBufferHeight;
|
||||
this.scrollArea.style.height = this.lastRecordedBufferHeight + 'px';
|
||||
private _refresh(): void {
|
||||
if (this._refreshAnimationFrame === null) {
|
||||
this._refreshAnimationFrame = requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
}
|
||||
|
||||
private _innerRefresh(): void {
|
||||
if (this._charMeasure.height > 0) {
|
||||
this._currentRowHeight = this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._terminal.renderer.dimensions.canvasHeight);
|
||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this._lastRecordedBufferHeight = newBufferHeight;
|
||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||
// Ignore the next scroll event which will be triggered by setting the scrollTop as we do not
|
||||
// want this event to scroll the terminal
|
||||
this._ignoreNextScrollEvent = true;
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
this._refreshAnimationFrame = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates dimensions and synchronizes the scroll area if necessary.
|
||||
*/
|
||||
public syncScrollArea(): void {
|
||||
if (this.lastRecordedBufferLength !== this.terminal.buffer.lines.length) {
|
||||
if (this._lastRecordedBufferLength !== this._terminal.buffer.lines.length) {
|
||||
// If buffer height changed
|
||||
this.lastRecordedBufferLength = this.terminal.buffer.lines.length;
|
||||
this.refresh();
|
||||
} else if (this.lastRecordedViewportHeight !== (<any>this.terminal).renderer.dimensions.canvasHeight) {
|
||||
this._lastRecordedBufferLength = this._terminal.buffer.lines.length;
|
||||
this._refresh();
|
||||
} else if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
||||
// If viewport height changed
|
||||
this.refresh();
|
||||
this._refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
if (this.terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this.currentRowHeight) {
|
||||
this.refresh();
|
||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh();
|
||||
}
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this.terminal.buffer.ydisp * this.currentRowHeight;
|
||||
if (this.viewportElement.scrollTop !== scrollTop) {
|
||||
this.viewportElement.scrollTop = scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,16 +117,23 @@ export class Viewport implements IViewport {
|
||||
* terminal to scroll to it.
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private onScroll(ev: Event): void {
|
||||
private _onScroll(ev: Event): void {
|
||||
// Don't attempt to scroll if the element is not visible, otherwise scrollTop will be corrupt
|
||||
// which causes the terminal to scroll the buffer to the top
|
||||
if (!this.viewportElement.offsetParent) {
|
||||
if (!this._viewportElement.offsetParent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this.viewportElement.scrollTop / this.currentRowHeight);
|
||||
const diff = newRow - this.terminal.buffer.ydisp;
|
||||
this.terminal.scrollLines(diff, true);
|
||||
// Ignore the event if it was flagged to ignore (when the source of the event is from Viewport)
|
||||
if (this._ignoreNextScrollEvent) {
|
||||
this._ignoreNextScrollEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._terminal.buffer.ydisp;
|
||||
this._terminal.scrollLines(diff, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,28 +143,61 @@ export class Viewport implements IViewport {
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public onWheel(ev: WheelEvent): void {
|
||||
if (ev.deltaY === 0) {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
const amount = this._getPixelsScrolled(ev);
|
||||
if (amount === 0) {
|
||||
return;
|
||||
}
|
||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||
let multiplier = 1;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
multiplier = this.currentRowHeight;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
multiplier = this.currentRowHeight * this.terminal.rows;
|
||||
}
|
||||
this.viewportElement.scrollTop += ev.deltaY * multiplier;
|
||||
this._viewportElement.scrollTop += amount;
|
||||
// Prevent the page from scrolling when the terminal scrolls
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
private _getPixelsScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_PIXEL
|
||||
let amount = ev.deltaY;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_LINE) {
|
||||
amount *= this._currentRowHeight;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._currentRowHeight * this._terminal.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of pixels scrolled by the mouse event taking into account what type of delta
|
||||
* is being used.
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public getLinesScrolled(ev: WheelEvent): number {
|
||||
// Do nothing if it's not a vertical scroll event
|
||||
if (ev.deltaY === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Fallback to WheelEvent.DOM_DELTA_LINE
|
||||
let amount = ev.deltaY;
|
||||
if (ev.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
|
||||
amount /= this._currentRowHeight + 0.0; // Prevent integer division
|
||||
this._wheelPartialScroll += amount;
|
||||
amount = Math.floor(Math.abs(this._wheelPartialScroll)) * (this._wheelPartialScroll > 0 ? 1 : -1);
|
||||
this._wheelPartialScroll %= 1;
|
||||
} else if (ev.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
|
||||
amount *= this._terminal.rows;
|
||||
}
|
||||
return amount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchStart(ev: TouchEvent): void {
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,12 +205,12 @@ export class Viewport implements IViewport {
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent): void {
|
||||
let deltaY = this.lastTouchY - ev.touches[0].pageY;
|
||||
this.lastTouchY = ev.touches[0].pageY;
|
||||
const deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
this.viewportElement.scrollTop += deltaY;
|
||||
this._viewportElement.scrollTop += deltaY;
|
||||
ev.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
|
||||
export interface IAttachAddonTerminal extends Terminal {
|
||||
_core: {
|
||||
register<T extends IDisposable>(d: T): void;
|
||||
};
|
||||
|
||||
__socket?: WebSocket;
|
||||
__attachSocketBuffer?: string;
|
||||
|
||||
|
||||
+54
-21
@@ -5,9 +5,7 @@
|
||||
* Implements the attach method, that attaches the terminal to a WebSocket stream.
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { Terminal, IDisposable } from 'xterm';
|
||||
import { IAttachAddonTerminal } from './Interfaces';
|
||||
|
||||
/**
|
||||
@@ -38,29 +36,50 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
}
|
||||
};
|
||||
|
||||
let myTextDecoder;
|
||||
// TODO: This should be typed but there seem to be issues importing the type
|
||||
let myTextDecoder: any;
|
||||
|
||||
addonTerminal.__getMessage = function(ev: MessageEvent): void {
|
||||
let str;
|
||||
let str: string;
|
||||
|
||||
if (typeof ev.data === 'object') {
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
if (!myTextDecoder) {
|
||||
myTextDecoder = new TextDecoder();
|
||||
}
|
||||
|
||||
str = myTextDecoder.decode( ev.data );
|
||||
} else {
|
||||
throw 'TODO: handle Blob?';
|
||||
if (!myTextDecoder) {
|
||||
myTextDecoder = new TextDecoder();
|
||||
}
|
||||
}
|
||||
if (ev.data instanceof ArrayBuffer) {
|
||||
str = myTextDecoder.decode(ev.data);
|
||||
displayData(str);
|
||||
} else {
|
||||
const fileReader = new FileReader();
|
||||
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(str || ev.data);
|
||||
fileReader.addEventListener('load', () => {
|
||||
str = myTextDecoder.decode(fileReader.result);
|
||||
displayData(str);
|
||||
});
|
||||
fileReader.readAsArrayBuffer(ev.data);
|
||||
}
|
||||
} else if (typeof ev.data === 'string') {
|
||||
displayData(ev.data);
|
||||
} else {
|
||||
addonTerminal.write(str || ev.data);
|
||||
throw Error(`Cannot handle "${typeof ev.data}" websocket message.`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Push data to buffer or write it in the terminal.
|
||||
* This is used as a callback for FileReader.onload.
|
||||
*
|
||||
* @param str String decoded by FileReader.
|
||||
* @param data The data of the EventMessage.
|
||||
*/
|
||||
function displayData(str?: string, data?: string): void {
|
||||
if (buffered) {
|
||||
addonTerminal.__pushToBuffer(str || data);
|
||||
} else {
|
||||
addonTerminal.write(str || data);
|
||||
}
|
||||
}
|
||||
|
||||
addonTerminal.__sendData = (data: string) => {
|
||||
if (socket.readyState !== 1) {
|
||||
return;
|
||||
@@ -68,14 +87,28 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
socket.send(data);
|
||||
};
|
||||
|
||||
socket.addEventListener('message', addonTerminal.__getMessage);
|
||||
addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage));
|
||||
|
||||
if (bidirectional) {
|
||||
addonTerminal.on('data', addonTerminal.__sendData);
|
||||
addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData));
|
||||
}
|
||||
|
||||
socket.addEventListener('close', () => detach(addonTerminal, socket));
|
||||
socket.addEventListener('error', () => detach(addonTerminal, socket));
|
||||
addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket)));
|
||||
addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket)));
|
||||
}
|
||||
|
||||
function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable {
|
||||
socket.addEventListener(type, handler);
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
socket.removeEventListener(type, handler);
|
||||
handler = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/attach/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
## fit addon
|
||||
|
||||
The fit addon adjusts the dimensions of the terminal to match best fit its parent element container. `fit` will only work when the element is visible.
|
||||
@@ -13,8 +13,6 @@
|
||||
* row and truncate its width with the current number of columns).
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface IGeometry {
|
||||
@@ -39,10 +37,10 @@ export function proposeGeometry(term: Terminal): IGeometry {
|
||||
const elementPaddingVer = elementPadding.top + elementPadding.bottom;
|
||||
const elementPaddingHor = elementPadding.right + elementPadding.left;
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term).viewport.scrollBarWidth;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - (<any>term)._core.viewport.scrollBarWidth;
|
||||
const geometry = {
|
||||
cols: Math.floor(availableWidth / (<any>term).renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term).renderer.dimensions.actualCellHeight)
|
||||
cols: Math.floor(availableWidth / (<any>term)._core.renderer.dimensions.actualCellWidth),
|
||||
rows: Math.floor(availableHeight / (<any>term)._core.renderer.dimensions.actualCellHeight)
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
@@ -52,7 +50,7 @@ export function fit(term: Terminal): void {
|
||||
if (geometry) {
|
||||
// Force a full render
|
||||
if (term.rows !== geometry.rows || term.cols !== geometry.cols) {
|
||||
(<any>term).renderer.clear();
|
||||
(<any>term)._core.renderer.clear();
|
||||
term.resize(geometry.cols, geometry.rows);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/fit/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/fullscreen/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,14 +5,17 @@
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface ISearchAddonTerminal extends Terminal {
|
||||
__searchHelper?: ISearchHelper;
|
||||
|
||||
// TODO: Reuse ITerminal from core
|
||||
// TODO: Don't rely on this private API
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
selectionManager: any;
|
||||
}
|
||||
|
||||
export interface ISearchAddonTerminal extends Terminal {
|
||||
__searchHelper?: ISearchHelper;
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
|
||||
export interface ISearchHelper {
|
||||
findNext(term: string): boolean;
|
||||
findPrevious(term: string): boolean;
|
||||
|
||||
@@ -35,14 +35,14 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal.buffer.ydisp;
|
||||
if (this._terminal.selectionManager.selectionEnd) {
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionEnd) {
|
||||
// Start from the selection end if there is a selection
|
||||
startRow = this._terminal.selectionManager.selectionEnd[1];
|
||||
startRow = this._terminal._core.selectionManager.selectionEnd[1];
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
for (let y = startRow + 1; y < this._terminal.buffer.ybase + this._terminal.rows; y++) {
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
@@ -76,10 +76,10 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
let result: ISearchResult;
|
||||
|
||||
let startRow = this._terminal.buffer.ydisp;
|
||||
if (this._terminal.selectionManager.selectionStart) {
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
if (this._terminal._core.selectionManager.selectionStart) {
|
||||
// Start from the selection end if there is a selection
|
||||
startRow = this._terminal.selectionManager.selectionStart[1];
|
||||
startRow = this._terminal._core.selectionManager.selectionStart[1];
|
||||
}
|
||||
|
||||
// Search from ydisp + 1 to end
|
||||
@@ -92,7 +92,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
|
||||
// Search from the top to the current ydisp
|
||||
if (!result) {
|
||||
for (let y = this._terminal.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
if (result) {
|
||||
break;
|
||||
@@ -111,13 +111,13 @@ export class SearchHelper implements ISearchHelper {
|
||||
* @return The search result if it was found.
|
||||
*/
|
||||
private _findInLine(term: string, y: number): ISearchResult {
|
||||
const lowerStringLine = this._terminal.buffer.translateBufferLineToString(y, true).toLowerCase();
|
||||
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
||||
const lowerTerm = term.toLowerCase();
|
||||
let searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
if (searchIndex >= 0) {
|
||||
const line = this._terminal.buffer.lines.get(y);
|
||||
const line = this._terminal._core.buffer.lines.get(y);
|
||||
for (let i = 0; i < searchIndex; i++) {
|
||||
const charData = line[i];
|
||||
const charData = line.get(i);
|
||||
// Adjust the searchIndex to normalize emoji into single chars
|
||||
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
|
||||
if (char.length > 1) {
|
||||
@@ -147,8 +147,8 @@ export class SearchHelper implements ISearchHelper {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
this._terminal.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal.buffer.ydisp);
|
||||
this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length);
|
||||
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
import * as search from './search';
|
||||
import { SearchHelper } from './SearchHelper';
|
||||
import { ISearchHelper } from './Interfaces';
|
||||
|
||||
|
||||
class MockTerminalPlain {}
|
||||
|
||||
class MockTerminal {
|
||||
private _core: any;
|
||||
public searchHelper: ISearchHelper;
|
||||
constructor(options: any) {
|
||||
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
||||
this.searchHelper = new SearchHelper(this as any);
|
||||
}
|
||||
get core(): any {
|
||||
return this._core;
|
||||
}
|
||||
pushWriteData(): void {
|
||||
this._core._innerWrite();
|
||||
}
|
||||
}
|
||||
|
||||
describe('search addon', function(): void {
|
||||
describe('apply', () => {
|
||||
it('should register findNext and findPrevious', () => {
|
||||
search.apply(<any>MockTerminalPlain);
|
||||
assert.equal(typeof (<any>MockTerminalPlain).prototype.findNext, 'function');
|
||||
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
|
||||
});
|
||||
});
|
||||
it('Searchhelper - should find correct position', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 3});
|
||||
term.core.write('Hello World\r\ntest\n123....hello');
|
||||
term.pushWriteData();
|
||||
const hello0 = (term.searchHelper as any)._findInLine('Hello', 0);
|
||||
const hello1 = (term.searchHelper as any)._findInLine('Hello', 1);
|
||||
const hello2 = (term.searchHelper as any)._findInLine('Hello', 2);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql(undefined);
|
||||
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
|
||||
});
|
||||
});
|
||||
@@ -3,8 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { SearchHelper } from './SearchHelper';
|
||||
import { Terminal } from 'xterm';
|
||||
import { ISearchAddonTerminal } from './Interfaces';
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/search/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
* WebSocket stream.
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { ITerminadoAddonTerminal } from './Interfaces';
|
||||
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/terminado/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "xterm.weblinks",
|
||||
"main": "weblinks.js",
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/webLinks/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert, expect } from 'chai';
|
||||
|
||||
import * as webLinks from './webLinks';
|
||||
|
||||
class MockTerminal {
|
||||
public regex: RegExp;
|
||||
public handler: (event: MouseEvent, uri: string) => void;
|
||||
public options?: any;
|
||||
|
||||
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number {
|
||||
this.regex = regex;
|
||||
this.handler = handler;
|
||||
this.options = options;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
describe('webLinks addon', () => {
|
||||
describe('apply', () => {
|
||||
it('should do register the `webLinksInit` method', () => {
|
||||
webLinks.apply(<any>MockTerminal);
|
||||
assert.equal(typeof (<any>MockTerminal).prototype.webLinksInit, 'function');
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow ~ character in URI path', () => {
|
||||
const term = new MockTerminal();
|
||||
webLinks.webLinksInit(<any>term);
|
||||
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ILinkMatcherOptions } from 'xterm';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,6})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathClause = '(\\/[\\/\\w\\.\\-%~]*)*';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
|
||||
function handleLink(event: MouseEvent, uri: string): void {
|
||||
window.open(uri, '_blank');
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the web links addon, registering the link matcher.
|
||||
* @param term The terminal to use web links within.
|
||||
* @param handler A custom handler to use.
|
||||
* @param options Custom options to use, matchIndex will always be ignored.
|
||||
*/
|
||||
export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void {
|
||||
options.matchIndex = 1;
|
||||
term.registerLinkMatcher(strictUrlRegex, handler, options);
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void {
|
||||
webLinksInit(this, handler, options);
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,10 @@
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface IWinptyCompatAddonTerminal extends Terminal {
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
}
|
||||
|
||||
export interface IWinptyCompatAddonTerminal extends Terminal {
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "xterm.winptyCompat",
|
||||
"name": "xterm.winptycompat",
|
||||
"main": "winptyCompat.js",
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/winptyCompat/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
import { IWinptyCompatAddonTerminal } from './Interfaces';
|
||||
|
||||
const CHAR_DATA_CODE_INDEX = 3;
|
||||
const NULL_CELL_CODE = 32;
|
||||
|
||||
export function winptyCompatInit(terminal: Terminal): void {
|
||||
const addonTerminal = <IWinptyCompatAddonTerminal>terminal;
|
||||
|
||||
@@ -28,12 +29,12 @@ export function winptyCompatInit(terminal: Terminal): void {
|
||||
// Windows when text reaches the end of the terminal it's likely going to be
|
||||
// wrapped.
|
||||
addonTerminal.on('linefeed', () => {
|
||||
const line = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y - 1);
|
||||
const lastChar = line[addonTerminal.cols - 1];
|
||||
const line = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y - 1);
|
||||
const lastChar = line.get(addonTerminal.cols - 1);
|
||||
|
||||
if (lastChar[3] !== 32 /* ' ' */) {
|
||||
const nextLine = addonTerminal.buffer.lines.get(addonTerminal.buffer.ybase + addonTerminal.buffer.y);
|
||||
(<any>nextLine).isWrapped = true;
|
||||
if (lastChar[CHAR_DATA_CODE_INDEX] !== NULL_CELL_CODE) {
|
||||
const nextLine = addonTerminal._core.buffer.lines.get(addonTerminal._core.buffer.ybase + addonTerminal._core.buffer.y);
|
||||
nextLine.isWrapped = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
"outDir": "../../../lib/addons/zmodem/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
/**
|
||||
@@ -38,22 +36,23 @@ import { Terminal } from 'xterm';
|
||||
|
||||
let zmodem;
|
||||
|
||||
export interface IZModemOptions {
|
||||
export interface IZmodemOptions {
|
||||
noTerminalWriteOutsideSession?: boolean;
|
||||
}
|
||||
|
||||
export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions = {}): void {
|
||||
function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
|
||||
const term = this;
|
||||
const senderFunc = (octets: ArrayLike<number>) => ws.send(new Uint8Array(octets));
|
||||
|
||||
let zsentry;
|
||||
|
||||
function _shouldWrite(): boolean {
|
||||
function shouldWrite(): boolean {
|
||||
return !!zsentry.get_confirmed_session() || !opts.noTerminalWriteOutsideSession;
|
||||
}
|
||||
|
||||
zsentry = new zmodem.Sentry({
|
||||
to_terminal: (octets: ArrayLike<number>) => {
|
||||
if (_shouldWrite()) {
|
||||
if (shouldWrite()) {
|
||||
term.write(
|
||||
String.fromCharCode.apply(String, octets)
|
||||
);
|
||||
@@ -71,7 +70,7 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
|
||||
// may be specific to xterm.js’s demo, ultimately we
|
||||
// should reject anything that isn’t binary.
|
||||
if (typeof evt.data === 'string') {
|
||||
if (_shouldWrite()) {
|
||||
if (shouldWrite()) {
|
||||
term.write(evt.data);
|
||||
}
|
||||
}
|
||||
@@ -85,8 +84,8 @@ export function zmodemAttach(term: Terminal, ws: WebSocket, opts: IZModemOptions
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
zmodem = (typeof window === 'object') ? (<any>window).ZModem : {Browser: null}; // Nullify browser for tests
|
||||
zmodem = (typeof window === 'object') ? (<any>window).Zmodem : {Browser: null}; // Nullify browser for tests
|
||||
|
||||
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach.bind(this, this);
|
||||
(<any>terminalConstructor.prototype).zmodemAttach = zmodemAttach;
|
||||
(<any>terminalConstructor.prototype).zmodemBrowser = zmodem.Browser;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
import { assert } from 'chai';
|
||||
import { CircularList } from './CircularList';
|
||||
|
||||
class TestCircularList<T> extends CircularList<T> {
|
||||
public get array(): T[] { return this._array; }
|
||||
}
|
||||
|
||||
describe('CircularList', () => {
|
||||
describe('push', () => {
|
||||
it('should push values onto the array', () => {
|
||||
@@ -36,7 +36,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
|
||||
// Reconstruct array, starting at index 0. Only transfer values from the
|
||||
// indexes 0 to length.
|
||||
let newArray = new Array<T>(newMaxLength);
|
||||
const newArray = new Array<T>(newMaxLength);
|
||||
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
|
||||
newArray[i] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
@@ -58,16 +58,6 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
this._length = newLength;
|
||||
}
|
||||
|
||||
public get forEach(): (callbackfn: (value: T, index: number) => void) => void {
|
||||
return (callbackfn: (value: T, index: number) => void) => {
|
||||
let i = 0;
|
||||
let length = this.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
callbackfn(this.get(i), i);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value at an index.
|
||||
*
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Disposable } from './Lifecycle';
|
||||
|
||||
class TestDisposable extends Disposable {
|
||||
public get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
}
|
||||
|
||||
describe('Disposable', () => {
|
||||
describe('register', () => {
|
||||
it('should register disposables', () => {
|
||||
const d = new TestDisposable();
|
||||
const d2 = {
|
||||
dispose: () => { throw new Error(); }
|
||||
};
|
||||
d.register(d2);
|
||||
assert.throws(() => d.dispose());
|
||||
});
|
||||
});
|
||||
describe('unregister', () => {
|
||||
it('should unregister disposables', () => {
|
||||
const d = new TestDisposable();
|
||||
const d2 = {
|
||||
dispose: () => { throw new Error(); }
|
||||
};
|
||||
d.register(d2);
|
||||
d.unregister(d2);
|
||||
assert.doesNotThrow(() => d.dispose());
|
||||
});
|
||||
});
|
||||
describe('dispose', () => {
|
||||
it('should set is disposed flag', () => {
|
||||
const d = new TestDisposable();
|
||||
assert.isFalse(d.isDisposed);
|
||||
d.dispose();
|
||||
assert.isTrue(d.isDisposed);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'xterm';
|
||||
|
||||
/**
|
||||
* A base class that can be extended to provide convenience methods for managing the lifecycle of an
|
||||
* object and its components.
|
||||
*/
|
||||
export abstract class Disposable implements IDisposable {
|
||||
protected _disposables: IDisposable[] = [];
|
||||
protected _isDisposed: boolean = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the object, triggering the `dispose` method on all registered IDisposables.
|
||||
*/
|
||||
public dispose(): void {
|
||||
this._isDisposed = true;
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a disposable object.
|
||||
* @param d The disposable to register.
|
||||
*/
|
||||
public register<T extends IDisposable>(d: T): void {
|
||||
this._disposables.push(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters a disposable object if it has been registered, if not do
|
||||
* nothing.
|
||||
* @param d The disposable to unregister.
|
||||
*/
|
||||
public unregister<T extends IDisposable>(d: T): void {
|
||||
const index = this._disposables.indexOf(d);
|
||||
if (index !== -1) {
|
||||
this._disposables.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* A keyboard event interface which does not depend on the DOM, KeyboardEvent implicitly extends
|
||||
* this event.
|
||||
*/
|
||||
export interface IKeyboardEvent {
|
||||
altKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
metaKey: boolean;
|
||||
keyCode: number;
|
||||
key: string;
|
||||
type: string;
|
||||
}
|
||||
@@ -77,3 +77,74 @@ export namespace C0 {
|
||||
/** Delete (Caret = ^?) */
|
||||
export const DEL = '\x7f';
|
||||
}
|
||||
|
||||
/**
|
||||
* C1 control codes
|
||||
* See = https://en.wikipedia.org/wiki/C0_and_C1_control_codes
|
||||
*/
|
||||
export namespace C1 {
|
||||
/** padding character */
|
||||
export const PAD = '\x80';
|
||||
/** High Octet Preset */
|
||||
export const HOP = '\x81';
|
||||
/** Break Permitted Here */
|
||||
export const BPH = '\x82';
|
||||
/** No Break Here */
|
||||
export const NBH = '\x83';
|
||||
/** Index */
|
||||
export const IND = '\x84';
|
||||
/** Next Line */
|
||||
export const NEL = '\x85';
|
||||
/** Start of Selected Area */
|
||||
export const SSA = '\x86';
|
||||
/** End of Selected Area */
|
||||
export const ESA = '\x87';
|
||||
/** Horizontal Tabulation Set */
|
||||
export const HTS = '\x88';
|
||||
/** Horizontal Tabulation With Justification */
|
||||
export const HTJ = '\x89';
|
||||
/** Vertical Tabulation Set */
|
||||
export const VTS = '\x8a';
|
||||
/** Partial Line Down */
|
||||
export const PLD = '\x8b';
|
||||
/** Partial Line Up */
|
||||
export const PLU = '\x8c';
|
||||
/** Reverse Index */
|
||||
export const RI = '\x8d';
|
||||
/** Single-Shift 2 */
|
||||
export const SS2 = '\x8e';
|
||||
/** Single-Shift 3 */
|
||||
export const SS3 = '\x8f';
|
||||
/** Device Control String */
|
||||
export const DCS = '\x90';
|
||||
/** Private Use 1 */
|
||||
export const PU1 = '\x91';
|
||||
/** Private Use 2 */
|
||||
export const PU2 = '\x92';
|
||||
/** Set Transmit State */
|
||||
export const STS = '\x93';
|
||||
/** Destructive backspace, intended to eliminate ambiguity about meaning of BS. */
|
||||
export const CCH = '\x94';
|
||||
/** Message Waiting */
|
||||
export const MW = '\x95';
|
||||
/** Start of Protected Area */
|
||||
export const SPA = '\x96';
|
||||
/** End of Protected Area */
|
||||
export const EPA = '\x97';
|
||||
/** Start of String */
|
||||
export const SOS = '\x98';
|
||||
/** Single Graphic Character Introducer */
|
||||
export const SGCI = '\x99';
|
||||
/** Single Character Introducer */
|
||||
export const SCI = '\x9a';
|
||||
/** Control Sequence Introducer */
|
||||
export const CSI = '\x9b';
|
||||
/** String Terminator */
|
||||
export const ST = '\x9c';
|
||||
/** Operating System Command */
|
||||
export const OSC = '\x9d';
|
||||
/** Privacy Message */
|
||||
export const PM = '\x9e';
|
||||
/** Application Program Command */
|
||||
export const APC = '\x9f';
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export const enum KeyboardResultType {
|
||||
SEND_KEY,
|
||||
SELECT_ALL,
|
||||
PAGE_UP,
|
||||
PAGE_DOWN
|
||||
}
|
||||
|
||||
export interface IKeyboardResult {
|
||||
type: KeyboardResultType;
|
||||
cancel: boolean;
|
||||
key: string | undefined;
|
||||
}
|
||||
|
||||
export interface ICharset {
|
||||
[key: string]: string;
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharset } from './Types';
|
||||
import { ICharset } from '../Types';
|
||||
|
||||
/**
|
||||
* The character sets supported by the terminal. These enable several languages
|
||||
@@ -0,0 +1,284 @@
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { evaluateKeyboardEvent } from './Keyboard';
|
||||
import { IKeyboardResult } from '../Types';
|
||||
|
||||
/**
|
||||
* A helper function for testing which allows passing in a partial event and defaults will be filled
|
||||
* in on it.
|
||||
*/
|
||||
function testEvaluateKeyboardEvent(partialEvent: {
|
||||
altKey?: boolean;
|
||||
ctrlKey?: boolean;
|
||||
shiftKey?: boolean;
|
||||
metaKey?: boolean;
|
||||
keyCode?: number;
|
||||
key?: string;
|
||||
type?: string;
|
||||
}, partialOptions: {
|
||||
applicationCursorMode?: boolean;
|
||||
isMac?: boolean;
|
||||
macOptionIsMeta?: boolean;
|
||||
} = {}): IKeyboardResult {
|
||||
const event = {
|
||||
altKey: partialEvent.altKey || false,
|
||||
ctrlKey: partialEvent.ctrlKey || false,
|
||||
shiftKey: partialEvent.shiftKey || false,
|
||||
metaKey: partialEvent.metaKey || false,
|
||||
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : undefined,
|
||||
key: partialEvent.key || '',
|
||||
type: partialEvent.type || ''
|
||||
};
|
||||
const options = {
|
||||
applicationCursorMode: partialOptions.applicationCursorMode || false,
|
||||
isMac: partialOptions.isMac || false,
|
||||
macOptionIsMeta: partialOptions.macOptionIsMeta || false
|
||||
};
|
||||
return evaluateKeyboardEvent(event, options.applicationCursorMode, options.isMac, options.macOptionIsMeta);
|
||||
}
|
||||
|
||||
describe('Keyboard', () => {
|
||||
describe('evaluateKeyEscapeSequence', () => {
|
||||
it('should return the correct escape sequence for unmodified keys', () => {
|
||||
// Backspace
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 8 }).key, '\x7f'); // ^?
|
||||
// Tab
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 9 }).key, '\t');
|
||||
// Return/enter
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 13 }).key, '\r'); // CR
|
||||
// Escape
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 27 }).key, '\x1b');
|
||||
// Page up, page down
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
|
||||
// End, Home
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
|
||||
// Left, up, right, down arrows
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 37 }).key, '\x1b[D'); // CSI D
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 38 }).key, '\x1b[A'); // CSI A
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 39 }).key, '\x1b[C'); // CSI C
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 40 }).key, '\x1b[B'); // CSI B
|
||||
// Insert
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
|
||||
// Delete
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
|
||||
// F1-F12
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
|
||||
});
|
||||
it('should return \\x1b[3;5~ for ctrl+delete', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
|
||||
});
|
||||
it('should return \\x1b[3;2~ for shift+delete', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
|
||||
});
|
||||
it('should return \\x1b[3;3~ for alt+delete', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
|
||||
});
|
||||
it('should return \\x1b[5D for ctrl+left', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1b[5C for ctrl+right', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
|
||||
});
|
||||
it('should return \\x1b[5A for ctrl+up', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
||||
});
|
||||
it('should return \\x1b[5B for ctrl+down', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
||||
});
|
||||
|
||||
describe('On non-macOS platforms', () => {
|
||||
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
it('should return \\x1b[5D for alt+left', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1b[5C for alt+right', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
|
||||
});
|
||||
it('should return \\x1ba for alt+a', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
|
||||
});
|
||||
});
|
||||
|
||||
describe('On macOS platforms', () => {
|
||||
it('should return \\x1bb for alt+left', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
|
||||
});
|
||||
it('should return \\x1bf for alt+right', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
|
||||
});
|
||||
it('should return undefined for alt+a', () => {
|
||||
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
|
||||
});
|
||||
});
|
||||
|
||||
describe('with macOptionIsMeta', () => {
|
||||
it('should return \\x1ba for alt+a', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true, macOptionIsMeta: true }).key, '\x1ba');
|
||||
});
|
||||
});
|
||||
|
||||
it('should return \\x1b[5A for alt+up', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
|
||||
});
|
||||
it('should return \\x1b[5B for alt+down', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
|
||||
});
|
||||
it('should return the correct escape sequence for modified F1-F12 keys', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
|
||||
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
|
||||
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
|
||||
});
|
||||
|
||||
// Characters using ctrl+alt sequences
|
||||
it('should return proper sequence for ctrl+alt+a', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
|
||||
});
|
||||
|
||||
// Characters using alt sequences (numbers)
|
||||
it('should return proper sequences for alt+0', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
|
||||
});
|
||||
it('should return proper sequences for alt+1', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
|
||||
});
|
||||
it('should return proper sequences for alt+2', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
|
||||
});
|
||||
it('should return proper sequences for alt+3', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
|
||||
});
|
||||
it('should return proper sequences for alt+4', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
|
||||
});
|
||||
it('should return proper sequences for alt+5', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
|
||||
});
|
||||
it('should return proper sequences for alt+6', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
|
||||
});
|
||||
it('should return proper sequences for alt+7', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
|
||||
});
|
||||
it('should return proper sequences for alt+8', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
|
||||
});
|
||||
it('should return proper sequences for alt+9', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
|
||||
});
|
||||
|
||||
// Characters using alt sequences (special chars)
|
||||
it('should return proper sequences for alt+;', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
|
||||
});
|
||||
it('should return proper sequences for alt+=', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
|
||||
});
|
||||
it('should return proper sequences for alt+,', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
|
||||
});
|
||||
it('should return proper sequences for alt+-', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
|
||||
});
|
||||
it('should return proper sequences for alt+.', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
|
||||
});
|
||||
it('should return proper sequences for alt+/', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
|
||||
});
|
||||
it('should return proper sequences for alt+~', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
|
||||
});
|
||||
it('should return proper sequences for alt+[', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
|
||||
});
|
||||
it('should return proper sequences for alt+\\', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
|
||||
});
|
||||
it('should return proper sequences for alt+]', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
|
||||
});
|
||||
it('should return proper sequences for alt+\'', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
|
||||
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
|
||||
});
|
||||
|
||||
it('should handle mobile arrow events', () => {
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }).key, '\x1b[A');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }, { applicationCursorMode: true }).key, '\x1bOA');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }).key, '\x1b[D');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }, { applicationCursorMode: true }).key, '\x1bOD');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }).key, '\x1b[C');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }, { applicationCursorMode: true }).key, '\x1bOC');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
|
||||
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,357 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IKeyboardEvent } from '../../common/Types';
|
||||
import { IKeyboardResult, KeyboardResultType } from '../Types';
|
||||
import { C0 } from '../../common/data/EscapeSequences';
|
||||
|
||||
// reg + shift key mappings for digits and special chars
|
||||
const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = {
|
||||
// digits 0-9
|
||||
48: ['0', ')'],
|
||||
49: ['1', '!'],
|
||||
50: ['2', '@'],
|
||||
51: ['3', '#'],
|
||||
52: ['4', '$'],
|
||||
53: ['5', '%'],
|
||||
54: ['6', '^'],
|
||||
55: ['7', '&'],
|
||||
56: ['8', '*'],
|
||||
57: ['9', '('],
|
||||
|
||||
// special chars
|
||||
186: [';', ':'],
|
||||
187: ['=', '+'],
|
||||
188: [',', '<'],
|
||||
189: ['-', '_'],
|
||||
190: ['.', '>'],
|
||||
191: ['/', '?'],
|
||||
192: ['`', '~'],
|
||||
219: ['[', '{'],
|
||||
220: ['\\', '|'],
|
||||
221: [']', '}'],
|
||||
222: ['\'', '"']
|
||||
};
|
||||
|
||||
export function evaluateKeyboardEvent(
|
||||
ev: IKeyboardEvent,
|
||||
applicationCursorMode: boolean,
|
||||
isMac: boolean,
|
||||
macOptionIsMeta: boolean
|
||||
): IKeyboardResult {
|
||||
const result: IKeyboardResult = {
|
||||
type: KeyboardResultType.SEND_KEY,
|
||||
// Whether to cancel event propogation (NOTE: this may not be needed since the event is
|
||||
// canceled at the end of keyDown
|
||||
cancel: false,
|
||||
// The new key even to emit
|
||||
key: undefined
|
||||
};
|
||||
const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0);
|
||||
switch (ev.keyCode) {
|
||||
case 0:
|
||||
if (ev.key === 'UIKeyInputUpArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OA';
|
||||
} else {
|
||||
result.key = C0.ESC + '[A';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputLeftArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OD';
|
||||
} else {
|
||||
result.key = C0.ESC + '[D';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputRightArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OC';
|
||||
} else {
|
||||
result.key = C0.ESC + '[C';
|
||||
}
|
||||
}
|
||||
else if (ev.key === 'UIKeyInputDownArrow') {
|
||||
if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OB';
|
||||
} else {
|
||||
result.key = C0.ESC + '[B';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 8:
|
||||
// backspace
|
||||
if (ev.shiftKey) {
|
||||
result.key = C0.BS; // ^H
|
||||
break;
|
||||
} else if (ev.altKey) {
|
||||
result.key = C0.ESC + C0.DEL; // \e ^?
|
||||
break;
|
||||
}
|
||||
result.key = C0.DEL; // ^?
|
||||
break;
|
||||
case 9:
|
||||
// tab
|
||||
if (ev.shiftKey) {
|
||||
result.key = C0.ESC + '[Z';
|
||||
break;
|
||||
}
|
||||
result.key = C0.HT;
|
||||
result.cancel = true;
|
||||
break;
|
||||
case 13:
|
||||
// return/enter
|
||||
result.key = C0.CR;
|
||||
result.cancel = true;
|
||||
break;
|
||||
case 27:
|
||||
// escape
|
||||
result.key = C0.ESC;
|
||||
result.cancel = true;
|
||||
break;
|
||||
case 37:
|
||||
// left-arrow
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
|
||||
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
// macOS uses different escape sequences than linux
|
||||
if (result.key === C0.ESC + '[1;3D') {
|
||||
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
|
||||
}
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OD';
|
||||
} else {
|
||||
result.key = C0.ESC + '[D';
|
||||
}
|
||||
break;
|
||||
case 39:
|
||||
// right-arrow
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
|
||||
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
// macOS uses different escape sequences than linux
|
||||
if (result.key === C0.ESC + '[1;3C') {
|
||||
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
|
||||
}
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OC';
|
||||
} else {
|
||||
result.key = C0.ESC + '[C';
|
||||
}
|
||||
break;
|
||||
case 38:
|
||||
// up-arrow
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
|
||||
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
if (result.key === C0.ESC + '[1;3A') {
|
||||
result.key = C0.ESC + '[1;5A';
|
||||
}
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OA';
|
||||
} else {
|
||||
result.key = C0.ESC + '[A';
|
||||
}
|
||||
break;
|
||||
case 40:
|
||||
// down-arrow
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
|
||||
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
|
||||
// http://unix.stackexchange.com/a/108106
|
||||
if (result.key === C0.ESC + '[1;3B') {
|
||||
result.key = C0.ESC + '[1;5B';
|
||||
}
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OB';
|
||||
} else {
|
||||
result.key = C0.ESC + '[B';
|
||||
}
|
||||
break;
|
||||
case 45:
|
||||
// insert
|
||||
if (!ev.shiftKey && !ev.ctrlKey) {
|
||||
// <Ctrl> or <Shift> + <Insert> are used to
|
||||
// copy-paste on some systems.
|
||||
result.key = C0.ESC + '[2~';
|
||||
}
|
||||
break;
|
||||
case 46:
|
||||
// delete
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[3;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[3~';
|
||||
}
|
||||
break;
|
||||
case 36:
|
||||
// home
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OH';
|
||||
} else {
|
||||
result.key = C0.ESC + '[H';
|
||||
}
|
||||
break;
|
||||
case 35:
|
||||
// end
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
|
||||
} else if (applicationCursorMode) {
|
||||
result.key = C0.ESC + 'OF';
|
||||
} else {
|
||||
result.key = C0.ESC + '[F';
|
||||
}
|
||||
break;
|
||||
case 33:
|
||||
// page up
|
||||
if (ev.shiftKey) {
|
||||
result.type = KeyboardResultType.PAGE_UP;
|
||||
} else {
|
||||
result.key = C0.ESC + '[5~';
|
||||
}
|
||||
break;
|
||||
case 34:
|
||||
// page down
|
||||
if (ev.shiftKey) {
|
||||
result.type = KeyboardResultType.PAGE_DOWN;
|
||||
} else {
|
||||
result.key = C0.ESC + '[6~';
|
||||
}
|
||||
break;
|
||||
case 112:
|
||||
// F1-F12
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P';
|
||||
} else {
|
||||
result.key = C0.ESC + 'OP';
|
||||
}
|
||||
break;
|
||||
case 113:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q';
|
||||
} else {
|
||||
result.key = C0.ESC + 'OQ';
|
||||
}
|
||||
break;
|
||||
case 114:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R';
|
||||
} else {
|
||||
result.key = C0.ESC + 'OR';
|
||||
}
|
||||
break;
|
||||
case 115:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S';
|
||||
} else {
|
||||
result.key = C0.ESC + 'OS';
|
||||
}
|
||||
break;
|
||||
case 116:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[15;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[15~';
|
||||
}
|
||||
break;
|
||||
case 117:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[17;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[17~';
|
||||
}
|
||||
break;
|
||||
case 118:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[18;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[18~';
|
||||
}
|
||||
break;
|
||||
case 119:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[19;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[19~';
|
||||
}
|
||||
break;
|
||||
case 120:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[20;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[20~';
|
||||
}
|
||||
break;
|
||||
case 121:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[21;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[21~';
|
||||
}
|
||||
break;
|
||||
case 122:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[23;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[23~';
|
||||
}
|
||||
break;
|
||||
case 123:
|
||||
if (modifiers) {
|
||||
result.key = C0.ESC + '[24;' + (modifiers + 1) + '~';
|
||||
} else {
|
||||
result.key = C0.ESC + '[24~';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// a-z and space
|
||||
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
|
||||
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
|
||||
result.key = String.fromCharCode(ev.keyCode - 64);
|
||||
} else if (ev.keyCode === 32) {
|
||||
// NUL
|
||||
result.key = String.fromCharCode(0);
|
||||
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
|
||||
// escape, file sep, group sep, record sep, unit sep
|
||||
result.key = String.fromCharCode(ev.keyCode - 51 + 27);
|
||||
} else if (ev.keyCode === 56) {
|
||||
// delete
|
||||
result.key = String.fromCharCode(127);
|
||||
} else if (ev.keyCode === 219) {
|
||||
// ^[ - Control Sequence Introducer (CSI)
|
||||
result.key = String.fromCharCode(27);
|
||||
} else if (ev.keyCode === 220) {
|
||||
// ^\ - String Terminator (ST)
|
||||
result.key = String.fromCharCode(28);
|
||||
} else if (ev.keyCode === 221) {
|
||||
// ^] - Operating System Command (OSC)
|
||||
result.key = String.fromCharCode(29);
|
||||
}
|
||||
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
|
||||
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
|
||||
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
|
||||
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
|
||||
if (key) {
|
||||
result.key = C0.ESC + key;
|
||||
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
|
||||
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
|
||||
result.key = C0.ESC + String.fromCharCode(keyCode);
|
||||
}
|
||||
} else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
|
||||
if (ev.keyCode === 65) { // cmd + a
|
||||
result.type = KeyboardResultType.SELECT_ALL;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -3,14 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
||||
import { C0 } from '../EscapeSequences';
|
||||
import { ITerminal, ICircularList, IBufferLine } from '../Types';
|
||||
import { C0 } from '../common/data/EscapeSequences';
|
||||
|
||||
enum Direction {
|
||||
Up = 'A',
|
||||
Down = 'B',
|
||||
Right = 'C',
|
||||
Left = 'D'
|
||||
const enum Direction {
|
||||
UP = 'A',
|
||||
DOWN = 'B',
|
||||
RIGHT = 'C',
|
||||
LEFT = 'D'
|
||||
}
|
||||
|
||||
export class AltClickHandler {
|
||||
@@ -18,7 +18,7 @@ export class AltClickHandler {
|
||||
private _startCol: number;
|
||||
private _endRow: number;
|
||||
private _endCol: number;
|
||||
private _lines: ICircularList<LineData>;
|
||||
private _lines: ICircularList<IBufferLine>;
|
||||
|
||||
constructor(
|
||||
private _mouseEvent: MouseEvent,
|
||||
@@ -28,7 +28,7 @@ export class AltClickHandler {
|
||||
this._startCol = this._terminal.buffer.x;
|
||||
this._startRow = this._terminal.buffer.y;
|
||||
|
||||
[this._endCol, this._endRow] = this._terminal.mouseHelper.getCoords(
|
||||
const coordinates = this._terminal.mouseHelper.getCoords(
|
||||
this._mouseEvent,
|
||||
this._terminal.element,
|
||||
this._terminal.charMeasure,
|
||||
@@ -36,17 +36,21 @@ export class AltClickHandler {
|
||||
this._terminal.cols,
|
||||
this._terminal.rows,
|
||||
false
|
||||
).map((coordinate: number) => {
|
||||
return coordinate - 1;
|
||||
});
|
||||
);
|
||||
|
||||
if (coordinates) {
|
||||
[this._endCol, this._endRow] = coordinates.map((coordinate: number) => {
|
||||
return coordinate - 1;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the escape sequences of arrows to the terminal
|
||||
*/
|
||||
public move(): void {
|
||||
if (this._mouseEvent.altKey) {
|
||||
this._terminal.send(this._arrowSequences());
|
||||
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
|
||||
this._terminal.handler(this._arrowSequences());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,9 +60,13 @@ export class AltClickHandler {
|
||||
* then moves to requested col.
|
||||
*/
|
||||
private _arrowSequences(): string {
|
||||
return this._resetStartingRow() +
|
||||
this._moveToRequestedRow() +
|
||||
this._moveToRequestedCol();
|
||||
// The alt buffer should try to navigate between rows
|
||||
if (!this._terminal.buffer.hasScrollback) {
|
||||
return this._resetStartingRow() + this._moveToRequestedRow() + this._moveToRequestedCol();
|
||||
}
|
||||
|
||||
// Only move horizontally for the normal buffer
|
||||
return this._moveHorizontallyOnly();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,17 +75,13 @@ export class AltClickHandler {
|
||||
* positioning.
|
||||
*/
|
||||
private _resetStartingRow(): string {
|
||||
let startRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
let endRow = this._endRow;
|
||||
|
||||
if (this._moveToRequestedRow().length === 0) {
|
||||
return '';
|
||||
} else {
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, this._startRow, this._startCol,
|
||||
this._startRow - this._wrappedRowsForRow(this._startRow), false
|
||||
).length, this._sequence(Direction.Left));
|
||||
}
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, this._startRow, this._startCol,
|
||||
this._startRow - this._wrappedRowsForRow(this._startRow), false
|
||||
).length, this._sequence(Direction.LEFT));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,10 +89,10 @@ export class AltClickHandler {
|
||||
* ignoring wrapped rows
|
||||
*/
|
||||
private _moveToRequestedRow(): string {
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
const endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
let rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
|
||||
const rowsToMove = Math.abs(startRow - endRow) - this._wrappedRowsCount();
|
||||
|
||||
return repeat(rowsToMove, this._sequence(this._verticalDirection()));
|
||||
}
|
||||
@@ -104,15 +108,20 @@ export class AltClickHandler {
|
||||
startRow = this._startRow;
|
||||
}
|
||||
|
||||
let endRow = this._endRow;
|
||||
let direction = this._horizontalDirection();
|
||||
const endRow = this._endRow;
|
||||
const direction = this._horizontalDirection();
|
||||
|
||||
return repeat(this._bufferLine(
|
||||
this._startCol, startRow, this._endCol, endRow,
|
||||
direction === Direction.Right
|
||||
direction === Direction.RIGHT
|
||||
).length, this._sequence(direction));
|
||||
}
|
||||
|
||||
private _moveHorizontallyOnly(): string {
|
||||
const direction = this._horizontalDirection();
|
||||
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility functions
|
||||
*/
|
||||
@@ -123,13 +132,13 @@ export class AltClickHandler {
|
||||
*/
|
||||
private _wrappedRowsCount(): number {
|
||||
let wrappedRows = 0;
|
||||
let startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
let endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
const startRow = this._startRow - this._wrappedRowsForRow(this._startRow);
|
||||
const endRow = this._endRow - this._wrappedRowsForRow(this._endRow);
|
||||
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
let direction = this._verticalDirection() === Direction.Up ? -1 : 1;
|
||||
const direction = this._verticalDirection() === Direction.UP ? -1 : 1;
|
||||
|
||||
if ((<any>this._lines.get(startRow + (direction * i))).isWrapped) {
|
||||
if (this._lines.get(startRow + (direction * i)).isWrapped) {
|
||||
wrappedRows++;
|
||||
}
|
||||
}
|
||||
@@ -143,12 +152,12 @@ export class AltClickHandler {
|
||||
*/
|
||||
private _wrappedRowsForRow(currentRow: number): number {
|
||||
let rowCount = 0;
|
||||
let lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
let lineWraps = this._lines.get(currentRow).isWrapped;
|
||||
|
||||
while (lineWraps && currentRow >= 0 && currentRow < this._terminal.rows) {
|
||||
rowCount++;
|
||||
currentRow--;
|
||||
lineWraps = (<any>this._lines.get(currentRow)).isWrapped;
|
||||
lineWraps = this._lines.get(currentRow).isWrapped;
|
||||
}
|
||||
|
||||
return rowCount;
|
||||
@@ -173,10 +182,9 @@ export class AltClickHandler {
|
||||
startRow <= this._endRow) || // down/right or same y/right
|
||||
(this._startCol >= this._endCol &&
|
||||
startRow < this._endRow)) { // down/left or same y/left
|
||||
return Direction.Right;
|
||||
} else {
|
||||
return Direction.Left;
|
||||
return Direction.RIGHT;
|
||||
}
|
||||
return Direction.LEFT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,10 +192,9 @@ export class AltClickHandler {
|
||||
*/
|
||||
private _verticalDirection(): Direction {
|
||||
if (this._startRow > this._endRow) {
|
||||
return Direction.Up;
|
||||
} else {
|
||||
return Direction.Down;
|
||||
return Direction.UP;
|
||||
}
|
||||
return Direction.DOWN;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,17 +4,22 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import * as Terminal from '../Terminal';
|
||||
import * as Clipboard from './Clipboard';
|
||||
|
||||
describe('evaluatePastedTextProcessing', () => {
|
||||
it('should replace carriage return + line feed with line feed on windows', () => {
|
||||
const pastedText = 'foo\r\nbar\r\n';
|
||||
const processedText = Clipboard.prepareTextForTerminal(pastedText, false);
|
||||
const windowsProcessedText = Clipboard.prepareTextForTerminal(pastedText, true);
|
||||
it('should replace carriage return and/or line feed with carriage return', () => {
|
||||
const pastedText = {
|
||||
unix: 'foo\nbar\n',
|
||||
windows: 'foo\r\nbar\r\n'
|
||||
};
|
||||
|
||||
assert.equal(processedText, 'foo\r\nbar\r\n');
|
||||
assert.equal(windowsProcessedText, 'foo\rbar\r');
|
||||
const processedText = {
|
||||
unix: Clipboard.prepareTextForTerminal(pastedText.unix),
|
||||
windows: Clipboard.prepareTextForTerminal(pastedText.windows)
|
||||
};
|
||||
|
||||
assert.equal(processedText.unix, 'foo\rbar\r');
|
||||
assert.equal(processedText.windows, 'foo\rbar\r');
|
||||
});
|
||||
it('should bracket pasted text in bracketedPasteMode', () => {
|
||||
const pastedText = 'foo bar';
|
||||
|
||||
@@ -18,11 +18,8 @@ declare var window: IWindow;
|
||||
* Prepares text to be pasted into the terminal by normalizing the line endings
|
||||
* @param text The pasted text that needs processing before inserting into the terminal
|
||||
*/
|
||||
export function prepareTextForTerminal(text: string, isMSWindows: boolean): string {
|
||||
if (isMSWindows) {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
return text;
|
||||
export function prepareTextForTerminal(text: string): string {
|
||||
return text.replace(/\r?\n/g, '\r');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,7 +35,7 @@ export function bracketTextForPaste(text: string, bracketedPasteMode: boolean):
|
||||
|
||||
/**
|
||||
* Binds copy functionality to the given terminal.
|
||||
* @param {ClipboardEvent} ev The original copy event to be handled
|
||||
* @param ev The original copy event to be handled
|
||||
*/
|
||||
export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManager: ISelectionManager): void {
|
||||
if (term.browser.isMSIE) {
|
||||
@@ -53,16 +50,16 @@ export function copyHandler(ev: ClipboardEvent, term: ITerminal, selectionManage
|
||||
|
||||
/**
|
||||
* Redirect the clipboard's data to the terminal's input handler.
|
||||
* @param {ClipboardEvent} ev The original paste event to be handled
|
||||
* @param {Terminal} term The terminal on which to apply the handled paste event
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function pasteHandler(ev: ClipboardEvent, term: ITerminal): void {
|
||||
ev.stopPropagation();
|
||||
|
||||
let text: string;
|
||||
|
||||
let dispatchPaste = function(text: string): void {
|
||||
text = prepareTextForTerminal(text, term.browser.isMSWindows);
|
||||
const dispatchPaste = function(text: string): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = bracketTextForPaste(text, term.bracketedPasteMode);
|
||||
term.handler(text);
|
||||
term.textarea.value = '';
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import * as attach from '../addons/attach/attach';
|
||||
|
||||
describe('Terminal', () => {
|
||||
it('should apply addons with Terminal.applyAddon', () => {
|
||||
Terminal.applyAddon(attach);
|
||||
// Test that addon was applied successfully, adding attach to Terminal's
|
||||
// prototype.
|
||||
assert.equal(typeof (<any>Terminal).prototype.attach, 'function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm';
|
||||
import { ITerminal } from '../Types';
|
||||
import { Terminal as TerminalCore } from '../Terminal';
|
||||
import * as Strings from '../Strings';
|
||||
|
||||
export class Terminal implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
|
||||
constructor(options?: ITerminalOptions) {
|
||||
this._core = new TerminalCore(options);
|
||||
}
|
||||
|
||||
public get element(): HTMLElement { return this._core.element; }
|
||||
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get markers(): IMarker[] { return this._core.markers; }
|
||||
public blur(): void {
|
||||
this._core.blur();
|
||||
}
|
||||
public focus(): void {
|
||||
this._core.focus();
|
||||
}
|
||||
public on(type: 'blur' | 'focus' | 'linefeed' | 'selection', listener: () => void): void;
|
||||
public on(type: 'data', listener: (...args: any[]) => void): void;
|
||||
public on(type: 'key', listener: (key?: string, event?: KeyboardEvent) => void): void;
|
||||
public on(type: 'keypress' | 'keydown', listener: (event?: KeyboardEvent) => void): void;
|
||||
public on(type: 'refresh', listener: (data?: { start: number; end: number; }) => void): void;
|
||||
public on(type: 'resize', listener: (data?: { cols: number; rows: number; }) => void): void;
|
||||
public on(type: 'scroll', listener: (ydisp?: number) => void): void;
|
||||
public on(type: 'title', listener: (title?: string) => void): void;
|
||||
public on(type: string, listener: (...args: any[]) => void): void;
|
||||
public on(type: any, listener: any): void {
|
||||
this._core.on(type, listener);
|
||||
}
|
||||
public off(type: string, listener: (...args: any[]) => void): void {
|
||||
this._core.off(type, listener);
|
||||
}
|
||||
public emit(type: string, data?: any): void {
|
||||
this._core.emit(type, data);
|
||||
}
|
||||
public addDisposableListener(type: string, handler: (...args: any[]) => void): IDisposable {
|
||||
return this._core.addDisposableListener(type, handler);
|
||||
}
|
||||
public resize(columns: number, rows: number): void {
|
||||
this._core.resize(columns, rows);
|
||||
}
|
||||
public writeln(data: string): void {
|
||||
this._core.writeln(data);
|
||||
}
|
||||
public open(parent: HTMLElement): void {
|
||||
this._core.open(parent);
|
||||
}
|
||||
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
|
||||
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
|
||||
}
|
||||
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number {
|
||||
return this._core.registerLinkMatcher(regex, handler, options);
|
||||
}
|
||||
public deregisterLinkMatcher(matcherId: number): void {
|
||||
this._core.deregisterLinkMatcher(matcherId);
|
||||
}
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
return this._core.registerCharacterJoiner(handler);
|
||||
}
|
||||
public deregisterCharacterJoiner(joinerId: number): void {
|
||||
this._core.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
public addMarker(cursorYOffset: number): IMarker {
|
||||
return this._core.addMarker(cursorYOffset);
|
||||
}
|
||||
public hasSelection(): boolean {
|
||||
return this._core.hasSelection();
|
||||
}
|
||||
public getSelection(): string {
|
||||
return this._core.getSelection();
|
||||
}
|
||||
public clearSelection(): void {
|
||||
this._core.clearSelection();
|
||||
}
|
||||
public selectAll(): void {
|
||||
this._core.selectAll();
|
||||
}
|
||||
public selectLines(start: number, end: number): void {
|
||||
this._core.selectLines(start, end);
|
||||
}
|
||||
public dispose(): void {
|
||||
this._core.dispose();
|
||||
}
|
||||
public destroy(): void {
|
||||
this._core.destroy();
|
||||
}
|
||||
public scrollLines(amount: number): void {
|
||||
this._core.scrollLines(amount);
|
||||
}
|
||||
public scrollPages(pageCount: number): void {
|
||||
this._core.scrollPages(pageCount);
|
||||
}
|
||||
public scrollToTop(): void {
|
||||
this._core.scrollToTop();
|
||||
}
|
||||
public scrollToBottom(): void {
|
||||
this._core.scrollToBottom();
|
||||
}
|
||||
public scrollToLine(line: number): void {
|
||||
this._core.scrollToLine(line);
|
||||
}
|
||||
public clear(): void {
|
||||
this._core.clear();
|
||||
}
|
||||
public write(data: string): void {
|
||||
this._core.write(data);
|
||||
}
|
||||
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string;
|
||||
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
|
||||
public getOption(key: 'colors'): string[];
|
||||
public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number;
|
||||
public getOption(key: 'handler'): (data: string) => void;
|
||||
public getOption(key: string): any;
|
||||
public getOption(key: any): any {
|
||||
return this._core.getOption(key);
|
||||
}
|
||||
public setOption(key: 'bellSound' | 'fontFamily' | 'termName', value: string): void;
|
||||
public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900'): void;
|
||||
public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void;
|
||||
public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void;
|
||||
public setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
|
||||
public setOption(key: 'colors', value: string[]): void;
|
||||
public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void;
|
||||
public setOption(key: 'handler', value: (data: string) => void): void;
|
||||
public setOption(key: 'theme', value: ITheme): void;
|
||||
public setOption(key: 'cols' | 'rows', value: number): void;
|
||||
public setOption(key: string, value: any): void;
|
||||
public setOption(key: any, value: any): void {
|
||||
this._core.setOption(key, value);
|
||||
}
|
||||
public refresh(start: number, end: number): void {
|
||||
this._core.refresh(start, end);
|
||||
}
|
||||
public reset(): void {
|
||||
this._core.reset();
|
||||
}
|
||||
public static applyAddon(addon: any): void {
|
||||
addon.apply(Terminal);
|
||||
}
|
||||
public static get strings(): ILocalizableStrings {
|
||||
return Strings;
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,11 @@
|
||||
*/
|
||||
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CharData, ITerminal, ITerminalOptions } from '../Types';
|
||||
import { acquireCharAtlas, CHAR_ATLAS_CELL_SPACING } from './CharAtlas';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
|
||||
export const INVERTED_DEFAULT_COLOR = -1;
|
||||
const DIM_OPACITY = 0.5;
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import BaseCharAtlas from './atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
@@ -21,7 +20,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
|
||||
private _charAtlas: HTMLCanvasElement | ImageBitmap;
|
||||
protected _charAtlas: BaseCharAtlas;
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
@@ -37,6 +36,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
// Draw the background if this is an opaque layer
|
||||
@@ -50,7 +53,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
public onFocus(terminal: ITerminal): void {}
|
||||
public onCursorMove(terminal: ITerminal): void {}
|
||||
public onGridChanged(terminal: ITerminal, startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {}
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {}
|
||||
|
||||
public onThemeChanged(terminal: ITerminal, colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
@@ -84,16 +87,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = null;
|
||||
const result = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
if (result instanceof HTMLCanvasElement) {
|
||||
this._charAtlas = result;
|
||||
} else {
|
||||
result.then(bitmap => this._charAtlas = bitmap);
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCharWidth, this._scaledCharHeight);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions, charSizeChanged: boolean): void {
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
@@ -110,9 +108,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this.clearAll();
|
||||
}
|
||||
|
||||
if (charSizeChanged) {
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
}
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
}
|
||||
|
||||
public abstract reset(terminal: ITerminal): void;
|
||||
@@ -182,7 +178,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
@@ -202,7 +198,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
@@ -222,7 +218,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected fillCharTrueColor(terminal: ITerminal, charData: CharData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false);
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
this._ctx.textBaseline = 'top';
|
||||
this._clipRow(terminal, y);
|
||||
this._ctx.fillText(
|
||||
@@ -232,12 +228,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a character at a cell. If possible this will draw using the character
|
||||
* atlas to reduce draw time.
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
* @param terminal The terminal.
|
||||
* @param char The character.
|
||||
* @param chars The character or characters.
|
||||
* @param code The character code.
|
||||
* @param width The width of the character.
|
||||
* @param width The width of the characters.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
@@ -245,81 +241,45 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected drawChar(terminal: ITerminal, char: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean): void {
|
||||
let colorIndex = 0;
|
||||
if (fg < 256) {
|
||||
colorIndex = fg + 2;
|
||||
} else {
|
||||
// If default color and bold
|
||||
if (bold && terminal.options.enableBold) {
|
||||
colorIndex = 1;
|
||||
}
|
||||
protected drawChars(terminal: ITerminal, chars: string, code: number, width: number, x: number, y: number, fg: number, bg: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8 && fg !== INVERTED_DEFAULT_COLOR;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
{chars, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
}
|
||||
const isAscii = code < 256;
|
||||
// A color is basic if it is one of the standard normal or bold weight
|
||||
// colors of the characters held in the char atlas. Note that this excludes
|
||||
// the normal weight _light_ color characters.
|
||||
const isBasicColor = (colorIndex > 1 && fg < 16) && (fg < 8 || bold);
|
||||
const isDefaultColor = fg >= 256;
|
||||
const isDefaultBackground = bg >= 256;
|
||||
if (this._charAtlas && isAscii && (isBasicColor || isDefaultColor) && isDefaultBackground) {
|
||||
// ImageBitmap's draw about twice as fast as from a canvas
|
||||
const charAtlasCellWidth = this._scaledCharWidth + CHAR_ATLAS_CELL_SPACING;
|
||||
const charAtlasCellHeight = this._scaledCharHeight + CHAR_ATLAS_CELL_SPACING;
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (dim) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Draw the non-bold version of the same color if bold is not enabled
|
||||
if (bold && !terminal.options.enableBold) {
|
||||
// Ignore default color as it's not touched above
|
||||
if (colorIndex > 1) {
|
||||
colorIndex -= 8;
|
||||
}
|
||||
}
|
||||
|
||||
this._ctx.drawImage(this._charAtlas,
|
||||
code * charAtlasCellWidth,
|
||||
colorIndex * charAtlasCellHeight,
|
||||
charAtlasCellWidth,
|
||||
this._scaledCharHeight,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop,
|
||||
charAtlasCellWidth,
|
||||
this._scaledCharHeight);
|
||||
} else {
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim);
|
||||
}
|
||||
// This draws the atlas (for debugging purposes)
|
||||
// this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
// this._ctx.drawImage(this._charAtlas, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a character at a 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.
|
||||
* Draws one or more characters at one or more cells. The character(s) will be
|
||||
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||
* right if the last character is a wide character.
|
||||
* @param terminal The terminal.
|
||||
* @param char The character.
|
||||
* @param chars The character.
|
||||
* @param width The width of the character.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChar(terminal: ITerminal, char: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean): void {
|
||||
private _drawUncachedChars(terminal: ITerminal, chars: string, width: number, fg: number, x: number, y: number, bold: boolean, dim: boolean, italic: boolean): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(terminal, bold);
|
||||
this._ctx.font = this._getFont(terminal, bold, italic);
|
||||
this._ctx.textBaseline = 'top';
|
||||
|
||||
if (fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background;
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[fg];
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground;
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
this._clipRow(terminal, y);
|
||||
@@ -330,7 +290,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
// Draw the character
|
||||
this._ctx.fillText(
|
||||
char,
|
||||
chars,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop);
|
||||
this._ctx.restore();
|
||||
@@ -356,10 +316,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param terminal The terminal.
|
||||
* @param isBold If we should use the bold fontWeight.
|
||||
*/
|
||||
protected _getFont(terminal: ITerminal, isBold: boolean): string {
|
||||
protected _getFont(terminal: ITerminal, isBold: boolean, isItalic: boolean): string {
|
||||
const fontWeight = isBold ? terminal.options.fontWeightBold : terminal.options.fontWeight;
|
||||
const fontStyle = isItalic ? 'italic' : '';
|
||||
|
||||
return `${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
return `${fontStyle} ${fontWeight} ${terminal.options.fontSize * window.devicePixelRatio}px ${terminal.options.fontFamily}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IColorSet } from './Types';
|
||||
import { isFirefox } from '../shared/utils/Browser';
|
||||
import { generateCharAtlas, ICharAtlasRequest } from '../shared/CharAtlasGenerator';
|
||||
|
||||
export const CHAR_ATLAS_CELL_SPACING = 1;
|
||||
|
||||
interface ICharAtlasConfig {
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: string;
|
||||
fontWeightBold: string;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
bitmap: HTMLCanvasElement | Promise<ImageBitmap>;
|
||||
config: ICharAtlasConfig;
|
||||
ownedBy: ITerminal[];
|
||||
}
|
||||
|
||||
let charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireCharAtlas(terminal: ITerminal, colors: IColorSet, scaledCharWidth: number, scaledCharHeight: number): HTMLCanvasElement | Promise<ImageBitmap> {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, terminal, colors);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(terminal);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.bitmap;
|
||||
} else {
|
||||
// The configs differ, release the terminal from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the terminal to the cache entry and return
|
||||
entry.ownedBy.push(terminal);
|
||||
return entry.bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
const canvasFactory = (width: number, height: number) => {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
};
|
||||
|
||||
const charAtlasConfig: ICharAtlasRequest = {
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
background: colors.background,
|
||||
foreground: colors.foreground,
|
||||
ansiColors: colors.ansi,
|
||||
devicePixelRatio: window.devicePixelRatio,
|
||||
allowTransparency: terminal.options.allowTransparency
|
||||
};
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
bitmap: generateCharAtlas(window, canvasFactory, charAtlasConfig),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.bitmap;
|
||||
}
|
||||
|
||||
function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
|
||||
const clonedColors = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: null,
|
||||
cursorAccent: null,
|
||||
selection: null,
|
||||
ansi: colors.ansi.slice(0, 16)
|
||||
};
|
||||
return {
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
allowTransparency: terminal.options.allowTransparency,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i] !== b.colors.ansi[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { assert } from 'chai';
|
||||
|
||||
import { MockTerminal, MockBuffer } from '../utils/TestUtils.test';
|
||||
import { CircularList } from '../common/CircularList';
|
||||
|
||||
import { ICharacterJoinerRegistry } from './Types';
|
||||
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
|
||||
import { BufferLine } from '../BufferLine';
|
||||
import { IBufferLine } from '../Types';
|
||||
|
||||
describe('CharacterJoinerRegistry', () => {
|
||||
let registry: ICharacterJoinerRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
const terminal = new MockTerminal();
|
||||
terminal.cols = 16;
|
||||
terminal.buffer = new MockBuffer();
|
||||
const lines = new CircularList<IBufferLine>(7);
|
||||
lines.set(0, lineData([['a -> b -> c -> d']]));
|
||||
lines.set(1, lineData([['a -> b => c -> d']]));
|
||||
lines.set(2, lineData([['a -> b -', 0xFFFFFFFF], ['> c -> d', 0]]));
|
||||
|
||||
lines.set(3, lineData([['no joined ranges']]));
|
||||
lines.set(4, new BufferLine());
|
||||
lines.set(5, lineData([['a', 0x11111111], [' -> b -> c -> '], ['d', 0x22222222]]));
|
||||
const line6 = lineData([['wi']]);
|
||||
line6.push([0, '¥', 2, '¥'.charCodeAt(0)]);
|
||||
line6.push([0, '', 0, null]);
|
||||
let sub = lineData([['deemo']]);
|
||||
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
|
||||
line6.push([0, '\xf0\x9f\x98\x81', 1, 128513]);
|
||||
line6.push([0, ' ', 1, ' '.charCodeAt(0)]);
|
||||
sub = lineData([['jiabc']]);
|
||||
for (let i = 0; i < sub.length; ++i) line6.push(sub.get(i));
|
||||
lines.set(6, line6);
|
||||
|
||||
(<MockBuffer>terminal.buffer).setLines(lines);
|
||||
terminal.buffer.ydisp = 0;
|
||||
registry = new CharacterJoinerRegistry(terminal);
|
||||
});
|
||||
|
||||
it('has no joiners upon creation', () => {
|
||||
assert.deepEqual(registry.getJoinedCharacters(0), []);
|
||||
});
|
||||
|
||||
it('returns ranges matched by the registered joiners', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('processes the input using all provided joiners', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [12, 14]]
|
||||
);
|
||||
|
||||
registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('removes deregistered joiners from future calls', () => {
|
||||
const joiner1 = registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
const joiner2 = registry.registerCharacterJoiner(substringJoiner('=>'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
|
||||
registry.deregisterCharacterJoiner(joiner1);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[[7, 9]]
|
||||
);
|
||||
|
||||
registry.deregisterCharacterJoiner(joiner2);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(1),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('doesn\'t process joins on differently-styled characters', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[2, 4], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty list of ranges if there is nothing to be joined', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(3),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns an empty list of ranges if the line is empty', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(4),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
it('returns false when trying to deregister a joiner that does not exist', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
assert.deepEqual(registry.deregisterCharacterJoiner(123), false);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[2, 4], [7, 9], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('doesn\'t process same-styled ranges that only have one character', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('a'));
|
||||
registry.registerCharacterJoiner(substringJoiner('b'));
|
||||
registry.registerCharacterJoiner(substringJoiner('d'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(5),
|
||||
[[5, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles ranges that extend all the way to the end of the line', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('-> d'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[12, 16]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles adjacent ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('->'));
|
||||
registry.registerCharacterJoiner(substringJoiner('> c '));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(2),
|
||||
[[2, 4], [8, 12], [12, 14]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles fullwidth characters in the middle of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('wi¥de'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[0, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles fullwidth characters at the end of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('wi¥'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[0, 4]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles emojis in the middle of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 ji'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[6, 13]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles emojis at the end of ranges', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('emo\xf0\x9f\x98\x81 '));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[6, 11]]
|
||||
);
|
||||
});
|
||||
|
||||
it('handles ranges after wide and emoji characters', () => {
|
||||
registry.registerCharacterJoiner(substringJoiner('abc'));
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(6),
|
||||
[[13, 16]]
|
||||
);
|
||||
});
|
||||
|
||||
describe('range merging', () => {
|
||||
it('inserts a new range before the existing ones', () => {
|
||||
registry.registerCharacterJoiner(() => [[1, 2], [2, 3]]);
|
||||
registry.registerCharacterJoiner(() => [[0, 1]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 1], [1, 2], [2, 3]]
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts in between two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[2, 4]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [2, 4], [4, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('inserts after the last range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[6, 8]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [4, 6], [6, 8]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the beginning of a range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[3, 5]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [3, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the end of a range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 4]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 4], [4, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('extends the last range', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[5, 7]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 2], [4, 7]]
|
||||
);
|
||||
});
|
||||
|
||||
it('connects two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 5]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 6]]
|
||||
);
|
||||
});
|
||||
|
||||
it('connects more than two ranges', () => {
|
||||
registry.registerCharacterJoiner(() => [[0, 2], [4, 6], [8, 10], [12, 14]]);
|
||||
registry.registerCharacterJoiner(() => [[1, 10]]);
|
||||
assert.deepEqual(
|
||||
registry.getJoinedCharacters(0),
|
||||
[[0, 10], [12, 14]]
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
type IPartialLineData = ([string] | [string, number]);
|
||||
|
||||
function lineData(data: IPartialLineData[]): IBufferLine {
|
||||
const tline = new BufferLine();
|
||||
for (let i = 0; i < data.length; ++i) {
|
||||
const line = data[i][0];
|
||||
const attr = <number>(data[i][1] || 0);
|
||||
line.split('').map(char => tline.push([attr, char, 1, char.charCodeAt(0)]));
|
||||
}
|
||||
return tline;
|
||||
}
|
||||
|
||||
function substringJoiner(substring: string): (sequence: string) => [number, number][] {
|
||||
return (sequence: string): [number, number][] => {
|
||||
const ranges: [number, number][] = [];
|
||||
let searchIndex = 0;
|
||||
let matchIndex = -1;
|
||||
|
||||
while ((matchIndex = sequence.indexOf(substring, searchIndex)) !== -1) {
|
||||
const matchEndIndex = matchIndex + substring.length;
|
||||
searchIndex = matchEndIndex;
|
||||
ranges.push([matchIndex, matchEndIndex]);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
import { ITerminal, IBufferLine } from '../Types';
|
||||
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
|
||||
|
||||
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
|
||||
|
||||
private _characterJoiners: ICharacterJoiner[] = [];
|
||||
private _nextCharacterJoinerId: number = 0;
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
const joiner: ICharacterJoiner = {
|
||||
id: this._nextCharacterJoinerId++,
|
||||
handler
|
||||
};
|
||||
|
||||
this._characterJoiners.push(joiner);
|
||||
return joiner.id;
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
for (let i = 0; i < this._characterJoiners.length; i++) {
|
||||
if (this._characterJoiners[i].id === joinerId) {
|
||||
this._characterJoiners.splice(i, 1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public getJoinedCharacters(row: number): [number, number][] {
|
||||
if (this._characterJoiners.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const line = this._terminal.buffer.lines.get(row);
|
||||
if (line.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const ranges: [number, number][] = [];
|
||||
const lineStr = this._terminal.buffer.translateBufferLineToString(row, true);
|
||||
|
||||
// Because some cells can be represented by multiple javascript characters,
|
||||
// we track the cell and the string indexes separately. This allows us to
|
||||
// translate the string ranges we get from the joiners back into cell ranges
|
||||
// for use when rendering
|
||||
let rangeStartColumn = 0;
|
||||
let currentStringIndex = 0;
|
||||
let rangeStartStringIndex = 0;
|
||||
let rangeAttr = line.get(0)[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
for (let x = 0; x < this._terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const chars = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const attr = charData[CHAR_DATA_ATTR_INDEX] >> 9;
|
||||
|
||||
if (width === 0) {
|
||||
// If this character is of width 0, skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// End of range
|
||||
if (attr !== rangeAttr) {
|
||||
// If we ended up with a sequence of more than one character,
|
||||
// look for ranges to join.
|
||||
if (x - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Reset our markers for a new range.
|
||||
rangeStartColumn = x;
|
||||
rangeStartStringIndex = currentStringIndex;
|
||||
rangeAttr = attr;
|
||||
}
|
||||
|
||||
currentStringIndex += chars.length;
|
||||
}
|
||||
|
||||
// Process any trailing ranges.
|
||||
if (this._terminal.cols - rangeStartColumn > 1) {
|
||||
const joinedRanges = this._getJoinedRanges(
|
||||
lineStr,
|
||||
rangeStartStringIndex,
|
||||
currentStringIndex,
|
||||
line,
|
||||
rangeStartColumn
|
||||
);
|
||||
for (let i = 0; i < joinedRanges.length; i++) {
|
||||
ranges.push(joinedRanges[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a segment of a line of text, find all ranges of text that should be
|
||||
* joined in a single rendering unit. Ranges are internally converted to
|
||||
* column ranges, rather than string ranges.
|
||||
* @param line String representation of the full line of text
|
||||
* @param startIndex Start position of the range to search in the string (inclusive)
|
||||
* @param endIndex End position of the range to search in the string (exclusive)
|
||||
*/
|
||||
private _getJoinedRanges(line: string, startIndex: number, endIndex: number, lineData: IBufferLine, startCol: number): [number, number][] {
|
||||
const text = line.substring(startIndex, endIndex);
|
||||
// At this point we already know that there is at least one joiner so
|
||||
// we can just pull its value and assign it directly rather than
|
||||
// merging it into an empty array, which incurs unnecessary writes.
|
||||
const joinedRanges: [number, number][] = this._characterJoiners[0].handler(text);
|
||||
for (let i = 1; i < this._characterJoiners.length; i++) {
|
||||
// We merge any overlapping ranges across the different joiners
|
||||
const joinerRanges = this._characterJoiners[i].handler(text);
|
||||
for (let j = 0; j < joinerRanges.length; j++) {
|
||||
CharacterJoinerRegistry._mergeRanges(joinedRanges, joinerRanges[j]);
|
||||
}
|
||||
}
|
||||
this._stringRangesToCellRanges(joinedRanges, lineData, startCol);
|
||||
return joinedRanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the provided ranges in-place to adjust for variations between
|
||||
* string length and cell width so that the range represents a cell range,
|
||||
* rather than the string range the joiner provides.
|
||||
* @param ranges String ranges containing start (inclusive) and end (exclusive) index
|
||||
* @param line Cell data for the relevant line in the terminal
|
||||
* @param startCol Offset within the line to start from
|
||||
*/
|
||||
private _stringRangesToCellRanges(ranges: [number, number][], line: IBufferLine, startCol: number): void {
|
||||
let currentRangeIndex = 0;
|
||||
let currentRangeStarted = false;
|
||||
let currentStringIndex = 0;
|
||||
let currentRange = ranges[currentRangeIndex];
|
||||
|
||||
// If we got through all of the ranges, stop searching
|
||||
if (!currentRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let x = startCol; x < this._terminal.cols; x++) {
|
||||
const charData = line.get(x);
|
||||
const width = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
const length = charData[CHAR_DATA_CHAR_INDEX].length;
|
||||
|
||||
// We skip zero-width characters when creating the string to join the text
|
||||
// so we do the same here
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Adjust the start of the range
|
||||
if (!currentRangeStarted && currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
}
|
||||
|
||||
// Adjust the end of the range
|
||||
if (currentRange[1] <= currentStringIndex) {
|
||||
currentRange[1] = x;
|
||||
|
||||
// We're finished with this range, so we move to the next one
|
||||
currentRange = ranges[++currentRangeIndex];
|
||||
|
||||
// If there are no more ranges left, stop searching
|
||||
if (!currentRange) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Ranges can be on adjacent characters. Because the end index of the
|
||||
// ranges are exclusive, this means that the index for the start of a
|
||||
// range can be the same as the end index of the previous range. To
|
||||
// account for the start of the next range, we check here just in case.
|
||||
if (currentRange[0] <= currentStringIndex) {
|
||||
currentRange[0] = x;
|
||||
currentRangeStarted = true;
|
||||
} else {
|
||||
currentRangeStarted = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Adjust the string index based on the character length to line up with
|
||||
// the column adjustment
|
||||
currentStringIndex += length;
|
||||
}
|
||||
|
||||
// If there is still a range left at the end, it must extend all the way to
|
||||
// the end of the line.
|
||||
if (currentRange) {
|
||||
currentRange[1] = this._terminal.cols;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges the range defined by the provided start and end into the list of
|
||||
* existing ranges. The merge is done in place on the existing range for
|
||||
* performance and is also returned.
|
||||
* @param ranges Existing range list
|
||||
* @param newRange Tuple of two numbers representing the new range to merge in.
|
||||
* @returns The ranges input with the new range merged in place
|
||||
*/
|
||||
private static _mergeRanges(ranges: [number, number][], newRange: [number, number]): [number, number][] {
|
||||
let inRange = false;
|
||||
for (let i = 0; i < ranges.length; i++) {
|
||||
const range = ranges[i];
|
||||
if (!inRange) {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 1: New range is before the search range
|
||||
ranges.splice(i, 0, newRange);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 2: New range is either wholly contained within the
|
||||
// search range or overlaps with the front of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[0] < range[1]) {
|
||||
// Case 3: New range either wholly contains the search range
|
||||
// or overlaps with the end of it
|
||||
range[0] = Math.min(newRange[0], range[0]);
|
||||
inRange = true;
|
||||
}
|
||||
|
||||
// Case 4: New range starts after the search range
|
||||
continue;
|
||||
} else {
|
||||
if (newRange[1] <= range[0]) {
|
||||
// Case 5: New range extends from previous range but doesn't
|
||||
// reach the current one
|
||||
ranges[i - 1][1] = newRange[1];
|
||||
return ranges;
|
||||
}
|
||||
|
||||
if (newRange[1] <= range[1]) {
|
||||
// Case 6: New range extends from prvious range into the
|
||||
// current range
|
||||
ranges[i - 1][1] = Math.max(newRange[1], range[1]);
|
||||
ranges.splice(i, 1);
|
||||
inRange = false;
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Case 7: New range extends from previous range past the
|
||||
// end of the current range
|
||||
ranges.splice(i, 1);
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
if (inRange) {
|
||||
// Case 8: New range extends past the last existing range
|
||||
ranges[ranges.length - 1][1] = newRange[1];
|
||||
} else {
|
||||
// Case 9: New range starts after the last existing range
|
||||
ranges.push(newRange);
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user