Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae61292b10 | ||
|
|
8fd4bdd678 | ||
|
|
a7b051c52f | ||
|
|
b92bbf578b | ||
|
|
d4ec50e7de | ||
|
|
5a1c71c1d5 | ||
|
|
bbf36fe091 | ||
|
|
990bf5c973 | ||
|
|
b28bba56df | ||
|
|
21f3534668 | ||
|
|
fefc6c6a34 | ||
|
|
57a6fe5b87 | ||
|
|
9e446a9a0e | ||
|
|
16c0ad858a | ||
|
|
750ec3870c | ||
|
|
e9d54bf011 | ||
|
|
408403e2a0 | ||
|
|
a30bb22c7a | ||
|
|
a531b4dd9b | ||
|
|
10687b138d | ||
|
|
1108d0746e | ||
|
|
005785b84d | ||
|
|
2e8e64eb4d | ||
|
|
f44d57b2e8 | ||
|
|
4941d5a19e | ||
|
|
1f2d223e64 | ||
|
|
d1991396e7 | ||
|
|
478d824fa7 | ||
|
|
39a7b18065 | ||
|
|
2c62c3a8c3 | ||
|
|
4463f8d810 | ||
|
|
c05b6bd885 | ||
|
|
290c95ad71 | ||
|
|
e736f1223f | ||
|
|
cbe7b21090 | ||
|
|
d9ec47ab7a | ||
|
|
94b20d40bf | ||
|
|
202734f10c | ||
|
|
4656bd433c | ||
|
|
f6d5abf7b3 | ||
|
|
6ea56cd85e | ||
|
|
4d7ce4da46 | ||
|
|
39047767e4 | ||
|
|
8d4b5cdc55 | ||
|
|
00af63b271 | ||
|
|
608d78b6ac | ||
|
|
ac3595e8b4 | ||
|
|
5284a5a742 | ||
|
|
842ee6c164 | ||
|
|
d0dc387921 | ||
|
|
7762ab96a5 | ||
|
|
6528ba84ff | ||
|
|
0bcf30a21b | ||
|
|
86c78e726d | ||
|
|
cf9cb80cb4 | ||
|
|
04f2d52f26 | ||
|
|
192f751862 | ||
|
|
de143b552d | ||
|
|
d37a8ce539 | ||
|
|
2f66ec452c | ||
|
|
4b68976dfb | ||
|
|
3143ad7f02 | ||
|
|
631e331077 | ||
|
|
70be55df75 | ||
|
|
14c20c5f32 | ||
|
|
57bbbda7e8 | ||
|
|
8664f54a76 | ||
|
|
9514bfd31c | ||
|
|
9ce2894353 | ||
|
|
5c873c1ce2 | ||
|
|
4d4d9914a8 | ||
|
|
a19673d835 | ||
|
|
f7dbadc79b | ||
|
|
50d600877c | ||
|
|
cd2ac929c3 | ||
|
|
7401567856 | ||
|
|
a8d9c92ae1 | ||
|
|
bd68853575 | ||
|
|
69a6c04620 | ||
|
|
68766d540f | ||
|
|
a09e6a6238 | ||
|
|
30f74bf5f3 | ||
|
|
2708a76501 | ||
|
|
c8c22e3ddf | ||
|
|
7fc1fd947f | ||
|
|
7e69c5d0db | ||
|
|
0068c477b9 | ||
|
|
74474da7f3 | ||
|
|
e6e079bfa1 | ||
|
|
7dc8118367 | ||
|
|
5aa6656971 | ||
|
|
8979eea810 | ||
|
|
7d2bdacf77 | ||
|
|
1edc732c55 | ||
|
|
e03c93fb81 | ||
|
|
d23c65d1db | ||
|
|
07b5bd0b64 | ||
|
|
35483b3bf4 | ||
|
|
6ebf17bf04 | ||
|
|
5d37a17383 | ||
|
|
dade9ba050 | ||
|
|
5909885d43 | ||
|
|
0446cf3aee | ||
|
|
3a97dbecde | ||
|
|
c88bc62ff9 | ||
|
|
a2bfbeaa74 | ||
|
|
ade3e0943c | ||
|
|
16de86f5ea | ||
|
|
fed579238a | ||
|
|
11426bb0d0 | ||
|
|
5e004b21a3 | ||
|
|
b2ba281916 | ||
|
|
bcf223e235 | ||
|
|
d5c17a0cc9 | ||
|
|
54ed988fd1 | ||
|
|
1460a34796 | ||
|
|
bc156294aa | ||
|
|
09028d4d86 | ||
|
|
c35306aaa6 | ||
|
|
3289eed2a2 | ||
|
|
a76d4a6055 | ||
|
|
12e38404a2 | ||
|
|
08af72be1a | ||
|
|
daa6fa0a66 | ||
|
|
4aa8e402a4 | ||
|
|
a494e80a9f | ||
|
|
a8fe4107a3 | ||
|
|
f2049694c8 | ||
|
|
e59fef87cc | ||
|
|
d943a5c890 | ||
|
|
715c078b6d | ||
|
|
f9717bc635 | ||
|
|
7ef18cca1e | ||
|
|
c291ab1058 | ||
|
|
25e0e97ef2 | ||
|
|
0f28ccb093 | ||
|
|
f4bb4a22d4 | ||
|
|
4eede85ef6 | ||
|
|
bdb6647ae0 | ||
|
|
0d89fcdeeb | ||
|
|
4ffc076f84 | ||
|
|
2ed3997aaa | ||
|
|
a0f9768fe8 | ||
|
|
8b334b01a6 | ||
|
|
c6029765a4 | ||
|
|
520dacac27 | ||
|
|
dcc1b7a3e3 | ||
|
|
27c3663470 | ||
|
|
599271bde4 | ||
|
|
2716413e66 | ||
|
|
e5465841f8 | ||
|
|
03e54cce7d | ||
|
|
88205511e6 | ||
|
|
374a8b70eb | ||
|
|
48cf73c152 | ||
|
|
1dc88d2e29 | ||
|
|
7c1b1e2eb8 | ||
|
|
fb72f25067 | ||
|
|
3618c6495b | ||
|
|
443b2dbc28 | ||
|
|
53e995a2d8 | ||
|
|
53193d0a47 | ||
|
|
92341c3e7b | ||
|
|
57b8e064a8 | ||
|
|
3a310f1597 | ||
|
|
4d0217f816 | ||
|
|
559e932a2e | ||
|
|
aa427010e1 | ||
|
|
1aec57e8c0 | ||
|
|
cbbf6bd654 | ||
|
|
33b65bbfaa | ||
|
|
8e39650b5f | ||
|
|
f148238117 | ||
|
|
6057586ff3 | ||
|
|
e2267fce92 | ||
|
|
784a142dac | ||
|
|
71aa7027bd | ||
|
|
ca76ebc396 | ||
|
|
54187b3597 | ||
|
|
d58f5cac01 | ||
|
|
b3e8980270 | ||
|
|
4d14e00034 | ||
|
|
e75578440e | ||
|
|
70afb13c9b | ||
|
|
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 | ||
|
|
1736c1e12d | ||
|
|
c00a3726f3 | ||
|
|
5ef399df6f | ||
|
|
f8e9d02566 | ||
|
|
d8afdb1b6b | ||
|
|
2b4c5adb3c | ||
|
|
6024e7c5fe | ||
|
|
aa1e1c5d94 | ||
|
|
f055e0ef03 | ||
|
|
2f618b37c1 | ||
|
|
02893dcbb8 | ||
|
|
a37449a8cd | ||
|
|
551ccc0f2f | ||
|
|
108aa5ab31 | ||
|
|
362d7698f7 | ||
|
|
edb06c6b2a | ||
|
|
6cefb4c634 | ||
|
|
cd3eea5b48 | ||
|
|
5b5eb48867 | ||
|
|
09624c18e0 | ||
|
|
52e71a2530 | ||
|
|
6cadd7c4a8 | ||
|
|
b14308f2c4 | ||
|
|
2c3949375e | ||
|
|
e3e4dcb5d1 | ||
|
|
e9f4d39f2b | ||
|
|
d777513b87 | ||
|
|
1d2f356cfd | ||
|
|
418f2c6cdf | ||
|
|
3cab8bede1 | ||
|
|
58ce5e2110 | ||
|
|
20ff107768 | ||
|
|
16393b228d | ||
|
|
0f8d22547e | ||
|
|
c069e94a4c | ||
|
|
a1a4cac113 | ||
|
|
25ff77af03 | ||
|
|
536b3a8def | ||
|
|
d090cbe4b9 | ||
|
|
9a32852e61 | ||
|
|
89cf45c514 | ||
|
|
0c200f2feb | ||
|
|
728eeebcc7 | ||
|
|
c7fdff2f40 | ||
|
|
2eeb7b8ea5 | ||
|
|
0535f5d1b3 | ||
|
|
aeae688b4c | ||
|
|
af392a6305 | ||
|
|
ba6fcddc43 | ||
|
|
dbc6b12ec2 | ||
|
|
645cd2eb2c | ||
|
|
b158176862 | ||
|
|
a1035269d5 | ||
|
|
f5375c3a99 | ||
|
|
a55a7f3695 | ||
|
|
826cca7088 | ||
|
|
0f3d82a2a5 | ||
|
|
dc14f5226d | ||
|
|
27a3347c3f | ||
|
|
2d77fcf2fb | ||
|
|
9d2bbd199c | ||
|
|
b72cdadd11 | ||
|
|
aaaec75d73 | ||
|
|
9eb107a6e9 | ||
|
|
c0ab2b9a7c | ||
|
|
a058395014 | ||
|
|
55993ada9b | ||
|
|
c8910cc5fe | ||
|
|
f9767073ba | ||
|
|
20760d09e4 | ||
|
|
73f40ef7d2 | ||
|
|
9b085d2bdd | ||
|
|
fb7e5d590c | ||
|
|
3010760352 | ||
|
|
82c16dee73 | ||
|
|
7e8dd9647f | ||
|
|
b594641b26 | ||
|
|
5df9808d4c |
@@ -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.
|
||||
+2
-1
@@ -18,7 +18,8 @@ fixtures/typings-test/*.js
|
||||
package-lock.json
|
||||
|
||||
# Directories needed for code coverage
|
||||
/coverage/
|
||||
coverage/
|
||||
.nyc_output/
|
||||
|
||||
# Keep bundled code out of Git
|
||||
dist/
|
||||
|
||||
-19
@@ -1,19 +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
|
||||
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,5 +1,7 @@
|
||||
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>
|
||||
@@ -19,6 +21,7 @@ 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>
|
||||
@@ -58,14 +61,17 @@ 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>
|
||||
@@ -76,6 +82,7 @@ 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>
|
||||
@@ -88,6 +95,7 @@ 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>
|
||||
@@ -96,6 +104,8 @@ 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>
|
||||
@@ -104,10 +114,12 @@ 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>
|
||||
@@ -115,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
-5
@@ -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).
|
||||
@@ -50,7 +51,3 @@ By contributing code to xterm.js you
|
||||
holder has explicitly granted the right to use it like this,
|
||||
through a compatible open source license or through a direct
|
||||
agreement with you.)
|
||||
|
||||
### Third party dependencies
|
||||
|
||||
We prefer to not include any non-dev third party dependencies in order to keep our code minimal, performant and secure. If you plan on adding a dependency on a third party library it's a good idea to discuss the need in an issue with the maintainers first.
|
||||
|
||||
@@ -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/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)
|
||||
[](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3)
|
||||
[](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,7 +42,7 @@ 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 \x1B[1;3;31mxterm.js\x1B[0m $ ')
|
||||
</script>
|
||||
@@ -57,6 +60,12 @@ The proposed way to load xterm.js is via the ES6 module syntax.
|
||||
import { Terminal } from 'xterm';
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
Addons are JavaScript modules that extend the `Terminal` prototype with new methods and attributes to provide additional functionality. There are a handful available in the main repository in the `src/addons` directory and you can even write your own, by using xterm.js' public API.
|
||||
@@ -74,6 +83,8 @@ 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()`.
|
||||
@@ -107,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.
|
||||
|
||||
@@ -149,79 +164,17 @@ computational environment for Jupyter, supporting interactive data science and s
|
||||
- [**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.
|
||||
- [**Juno**](http://junolab.org/): A flexible Julia IDE, based on Atom.
|
||||
|
||||
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).
|
||||
|
||||
Xterm.js [provides a pre-built Docker image](https://hub.docker.com/r/xtermjs/xterm.js/) to help run the demo easily (Git tags are built as [tagged Docker images](https://hub.docker.com/r/xtermjs/xterm.js/tags/) too).
|
||||
|
||||
To run the just demo (with no editing access). run the following command in your terminal:
|
||||
|
||||
```
|
||||
docker run -p 3000:3000 xtermjs/xterm.js
|
||||
```
|
||||
|
||||
Then open http://0.0.0.0:3000 in a web browser to access the demo.
|
||||
|
||||
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
|
||||
|
||||
@@ -229,20 +182,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)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
# Node.js
|
||||
# Build a general Node.js application with npm.
|
||||
# Add steps that analyze code, save build artifacts, deploy, and more:
|
||||
# https://docs.microsoft.com/vsts/pipelines/languages/javascript
|
||||
|
||||
jobs:
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: 'ubuntu-16.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2
|
||||
inputs:
|
||||
versionSpec: "1.9.4"
|
||||
displayName: 'Install Yarn'
|
||||
- script: |
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
|
||||
- job: macOS
|
||||
pool:
|
||||
vmImage: 'xcode9-macos10.13'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: |
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
- script: |
|
||||
yarn test-coverage
|
||||
export COVERALLS_GIT_BRANCH=$BUILD_SOURCEBRANCH
|
||||
yarn coveralls
|
||||
displayName: 'Generate and publish coverage'
|
||||
|
||||
- job: Windows
|
||||
pool:
|
||||
vmImage: 'vs2017-win2016'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
versionSpec: '8.x'
|
||||
displayName: 'Install Node.js'
|
||||
- script: |
|
||||
yarn
|
||||
displayName: 'Install dependencies and build'
|
||||
- script: |
|
||||
yarn mocha
|
||||
displayName: 'Test'
|
||||
- script: |
|
||||
yarn lint
|
||||
displayName: 'Lint'
|
||||
+293
@@ -0,0 +1,293 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* This file is the entry point for browserify.
|
||||
*/
|
||||
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
import * as attach from '../lib/addons/attach/attach';
|
||||
import * as fit from '../lib/addons/fit/fit';
|
||||
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||
import * as search from '../lib/addons/search/search';
|
||||
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
import { Terminal as TerminalType } from 'xterm';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: TerminalType;
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
let term;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
let pid;
|
||||
|
||||
const terminalContainer = document.getElementById('terminal-container');
|
||||
const actionElements = {
|
||||
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous')
|
||||
};
|
||||
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||
|
||||
function setPadding(): void {
|
||||
term.element.style.padding = parseInt(paddingElement.value, 10).toString() + 'px';
|
||||
term.fit();
|
||||
}
|
||||
|
||||
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(): void {
|
||||
// Clean terminal
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
term = new Terminal({});
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.on('resize', (size: { cols: number, rows: number }) => {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
const cols = size.cols;
|
||||
const rows = size.rows;
|
||||
const url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
|
||||
addDomListener(actionElements.findNext, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findNext(actionElements.findNext.value, searchOptions);
|
||||
}
|
||||
});
|
||||
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
const searchOptions = {
|
||||
regex: (document.getElementById('regex') as HTMLInputElement).checked,
|
||||
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
|
||||
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
|
||||
};
|
||||
term.findPrevious(actionElements.findPrevious.value, searchOptions);
|
||||
}
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
initOptions(term);
|
||||
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
|
||||
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
|
||||
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
|
||||
paddingElement.value = '0';
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
updateTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then((res) => {
|
||||
res.text().then((processId) => {
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function runRealTerminal(): void {
|
||||
term.attach(socket);
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
function runFakeTerminal(): void {
|
||||
if (term._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
term._initialized = true;
|
||||
|
||||
term.prompt = () => {
|
||||
term.write('\r\n$ ');
|
||||
};
|
||||
|
||||
term.writeln('Welcome to xterm.js');
|
||||
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term.writeln('Type some keys and commands to play around.');
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term._core.register(term.addDisposableListener('key', (key, ev) => {
|
||||
const printable = !ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey;
|
||||
|
||||
if (ev.keyCode === 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode === 8) {
|
||||
// Do not delete the prompt
|
||||
if (term.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
}
|
||||
}));
|
||||
|
||||
term._core.register(term.addDisposableListener('paste', (data, ev) => {
|
||||
term.write(data);
|
||||
}));
|
||||
}
|
||||
|
||||
function initOptions(term: TerminalType): void {
|
||||
const blacklistedOptions = [
|
||||
// Internal only options
|
||||
'cancelEvents',
|
||||
'convertEol',
|
||||
'debug',
|
||||
'handler',
|
||||
'screenKeys',
|
||||
'termName',
|
||||
'useFlowControl',
|
||||
// Complex option
|
||||
'theme'
|
||||
];
|
||||
const 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']
|
||||
};
|
||||
const options = Object.keys((<any>term)._core.options);
|
||||
const booleanOptions = [];
|
||||
const 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}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let 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>';
|
||||
|
||||
const container = document.getElementById('options-container');
|
||||
container.innerHTML = html;
|
||||
|
||||
// Attach listeners
|
||||
booleanOptions.forEach(o => {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.checked);
|
||||
term.setOption(o, input.checked);
|
||||
});
|
||||
});
|
||||
numberOptions.forEach(o => {
|
||||
const input = <HTMLInputElement>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 => {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
term.setOption(o, input.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function addDomListener(element: HTMLElement, type: string, handler: (...args: any[]) => any): void {
|
||||
element.addEventListener(type, handler);
|
||||
term._core.register({ dispose: () => element.removeEventListener(type, handler) });
|
||||
}
|
||||
|
||||
function updateTerminalSize(): void {
|
||||
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
|
||||
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
|
||||
const width = (cols * term._core.renderer.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core.renderer.dimensions.actualCellHeight).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
term.fit();
|
||||
}
|
||||
+38
-92
@@ -1,95 +1,41 @@
|
||||
<!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 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></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><input type="checkbox" id="option-transparency"> transparency</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>
|
||||
<p>
|
||||
<label>
|
||||
experimentalCharAtlas
|
||||
<select id="option-experimental-char-atlas">
|
||||
<option value="static" selected>static</option>
|
||||
<option value="dynamic">dynamic</option>
|
||||
<option value="none">none</option>
|
||||
</select>
|
||||
</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>
|
||||
<h3>Accessibility</h3>
|
||||
<p>
|
||||
<label><input type="checkbox" id="option-screen-reader-mode"> screenReaderMode</label>
|
||||
</p>
|
||||
</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>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></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/client-bundle.js" defer ></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
-207
@@ -1,207 +0,0 @@
|
||||
import * as Terminal from '../build/xterm';
|
||||
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';
|
||||
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
Terminal.applyAddon(winptyCompat);
|
||||
|
||||
|
||||
var term,
|
||||
protocol,
|
||||
socketURL,
|
||||
socket,
|
||||
pid;
|
||||
|
||||
var terminalContainer = document.getElementById('terminal-container'),
|
||||
actionElements = {
|
||||
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'),
|
||||
transparency: document.querySelector('#option-transparency'),
|
||||
tabstopwidth: document.querySelector('#option-tabstopwidth'),
|
||||
experimentalCharAtlas: document.querySelector('#option-experimental-char-atlas'),
|
||||
bellStyle: document.querySelector('#option-bell-style'),
|
||||
screenReaderMode: document.querySelector('#option-screen-reader-mode')
|
||||
},
|
||||
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.macOptionIsMeta.addEventListener('change', function () {
|
||||
term.setOption('macOptionIsMeta', optionElements.macOptionIsMeta.checked);
|
||||
});
|
||||
optionElements.transparency.addEventListener('change', function () {
|
||||
var checked = optionElements.transparency.checked;
|
||||
term.setOption('allowTransparency', checked);
|
||||
term.setOption('theme', checked ? {background: 'rgba(0, 0, 0, .5)'} : {});
|
||||
});
|
||||
optionElements.cursorStyle.addEventListener('change', function () {
|
||||
term.setOption('cursorStyle', optionElements.cursorStyle.value);
|
||||
});
|
||||
optionElements.bellStyle.addEventListener('change', function () {
|
||||
term.setOption('bellStyle', optionElements.bellStyle.value);
|
||||
});
|
||||
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));
|
||||
});
|
||||
optionElements.experimentalCharAtlas.addEventListener('change', function () {
|
||||
term.setOption('experimentalCharAtlas', optionElements.experimentalCharAtlas.value);
|
||||
});
|
||||
optionElements.screenReaderMode.addEventListener('change', function () {
|
||||
term.setOption('screenReaderMode', optionElements.screenReaderMode.checked);
|
||||
});
|
||||
|
||||
createTerminal();
|
||||
|
||||
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),
|
||||
screenReaderMode: optionElements.screenReaderMode.checked
|
||||
});
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.on('resize', function (size) {
|
||||
if (!pid) {
|
||||
return;
|
||||
}
|
||||
var cols = size.cols,
|
||||
rows = size.rows,
|
||||
url = '/terminals/' + pid + '/size?cols=' + cols + '&rows=' + rows;
|
||||
|
||||
fetch(url, {method: 'POST'});
|
||||
});
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
term.winptyCompatInit();
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(function () {
|
||||
colsElement.value = term.cols;
|
||||
rowsElement.value = term.rows;
|
||||
paddingElement.value = 0;
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
setTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, {method: 'POST'}).then(function (res) {
|
||||
|
||||
res.text().then(function (processId) {
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
});
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function runRealTerminal() {
|
||||
term.attach(socket);
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
function runFakeTerminal() {
|
||||
if (term._initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
term._initialized = true;
|
||||
|
||||
var shellprompt = '$ ';
|
||||
|
||||
term.prompt = function () {
|
||||
term.write('\r\n' + shellprompt);
|
||||
};
|
||||
|
||||
term.writeln('Welcome to xterm.js');
|
||||
term.writeln('This is a local terminal emulation, without a real terminal in the back-end.');
|
||||
term.writeln('Type some keys and commands to play around.');
|
||||
term.writeln('');
|
||||
term.prompt();
|
||||
|
||||
term.on('key', function (key, ev) {
|
||||
var printable = (
|
||||
!ev.altKey && !ev.altGraphKey && !ev.ctrlKey && !ev.metaKey
|
||||
);
|
||||
|
||||
if (ev.keyCode == 13) {
|
||||
term.prompt();
|
||||
} else if (ev.keyCode == 8) {
|
||||
// Do not delete the prompt
|
||||
if (term.x > 2) {
|
||||
term.write('\b \b');
|
||||
}
|
||||
} else if (printable) {
|
||||
term.write(key);
|
||||
}
|
||||
});
|
||||
|
||||
term.on('paste', function (data, ev) {
|
||||
term.write(data);
|
||||
});
|
||||
}
|
||||
@@ -17,12 +17,8 @@ app.get('/style.css', function(req, res){
|
||||
res.sendFile(__dirname + '/style.css');
|
||||
});
|
||||
|
||||
app.get('/dist/bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/bundle.js');
|
||||
});
|
||||
|
||||
app.get('/dist/bundle.js.map', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/bundle.js.map');
|
||||
app.get('/dist/client-bundle.js', function(req, res){
|
||||
res.sendFile(__dirname + '/dist/client-bundle.js');
|
||||
});
|
||||
|
||||
app.post('/terminals', function (req, res) {
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* This file is the entry point for browserify.
|
||||
*/
|
||||
|
||||
const cp = require('child_process');
|
||||
const path = require('path');
|
||||
const webpack = require('webpack');
|
||||
|
||||
// Launch server
|
||||
cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' });
|
||||
|
||||
// Build/watch client source
|
||||
const clientConfig = {
|
||||
entry: path.resolve(__dirname, 'client.ts'),
|
||||
devtool: 'inline-source-map',
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.tsx?$/,
|
||||
use: 'ts-loader',
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
filename: 'client-bundle.js',
|
||||
path: path.resolve(__dirname, 'dist')
|
||||
},
|
||||
mode: 'development',
|
||||
watch: true
|
||||
};
|
||||
const compiler = webpack(clientConfig);
|
||||
|
||||
compiler.watch({
|
||||
// Example watchOptions
|
||||
aggregateTimeout: 300,
|
||||
poll: undefined
|
||||
}, (err, stats) => {
|
||||
// Print watch/build result here...
|
||||
console.log(stats.toString({
|
||||
colors: true
|
||||
}));
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"rootDir": ".",
|
||||
"sourceMap": true
|
||||
},
|
||||
"include": [
|
||||
"client.ts",
|
||||
"../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -4,6 +4,11 @@
|
||||
],
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5"
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6"
|
||||
],
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
|
||||
+22
-90
@@ -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');
|
||||
@@ -16,73 +14,40 @@ const source = require('vinyl-source-stream');
|
||||
const sourcemaps = require('gulp-sourcemaps');
|
||||
const ts = require('gulp-typescript');
|
||||
const util = require('gulp-util');
|
||||
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 = 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.
|
||||
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 +73,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 +99,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));
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -186,24 +136,6 @@ gulp.task('sorcery-addons', ['browserify-addons'], function () {
|
||||
})
|
||||
});
|
||||
|
||||
gulp.task('webpack', ['build'], function() {
|
||||
return gulp.src('demo/main.js')
|
||||
.pipe(webpack(require('./webpack.config.js')))
|
||||
.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('build', ['sorcery', 'sorcery-addons']);
|
||||
gulp.task('test', ['mocha']);
|
||||
gulp.task('default', ['build']);
|
||||
|
||||
+26
-17
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "3.4.0",
|
||||
"main": "lib/Terminal.js",
|
||||
"version": "3.8.0",
|
||||
"main": "lib/public/Terminal.js",
|
||||
"types": "typings/xterm.d.ts",
|
||||
"repository": "https://github.com/xtermjs/xterm.js",
|
||||
"license": "MIT",
|
||||
"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.108",
|
||||
"@types/text-encoding": "0.0.32",
|
||||
"@types/webpack": "^4.4.11",
|
||||
"browserify": "^13.3.0",
|
||||
"chai": "3.5.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",
|
||||
@@ -21,40 +24,46 @@
|
||||
"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",
|
||||
"ts-loader": "^4.5.0",
|
||||
"tslint": "^5.9.1",
|
||||
"tslint-consistent-codestyle": "^1.13.0",
|
||||
"typescript": "~2.7.1",
|
||||
"typescript": "3.0",
|
||||
"vinyl-buffer": "^1.0.0",
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-stream": "^4.0.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"zmodem.js": "^0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node demo/app",
|
||||
"start": "node demo/start",
|
||||
"start-zmodem": "node demo/zmodem/app",
|
||||
"lint": "tslint 'src/**/*.ts'",
|
||||
"test": "npm-run-all mocha lint",
|
||||
"lint": "tslint 'src/**/*.ts' './demo/**/*.ts'",
|
||||
"test": "npm run mocha",
|
||||
"posttest": "npm run 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"
|
||||
"watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"",
|
||||
"watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"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\""
|
||||
}
|
||||
}
|
||||
|
||||
+23
-22
@@ -6,9 +6,9 @@
|
||||
import * as Strings from './Strings';
|
||||
import { ITerminal, IBuffer } from './Types';
|
||||
import { isMac } from './shared/utils/Browser';
|
||||
import { RenderDebouncer } from './utils/RenderDebouncer';
|
||||
import { addDisposableListener } from './utils/Dom';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { RenderDebouncer } from './ui/RenderDebouncer';
|
||||
import { addDisposableDomListener } from './ui/Lifecycle';
|
||||
import { Disposable } from './common/Lifecycle';
|
||||
|
||||
const MAX_ROWS_TO_READ = 20;
|
||||
|
||||
@@ -17,7 +17,7 @@ const enum BoundaryPosition {
|
||||
BOTTOM
|
||||
}
|
||||
|
||||
export class AccessibilityManager implements IDisposable {
|
||||
export class AccessibilityManager extends Disposable {
|
||||
private _accessibilityTreeRoot: HTMLElement;
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[];
|
||||
@@ -29,8 +29,6 @@ export class AccessibilityManager implements IDisposable {
|
||||
private _topBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
private _bottomBoundaryFocusListener: (e: FocusEvent) => void;
|
||||
|
||||
private _disposables: IDisposable[] = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -43,6 +41,7 @@ export class AccessibilityManager implements IDisposable {
|
||||
private _charsToConsume: string[] = [];
|
||||
|
||||
constructor(private _terminal: ITerminal) {
|
||||
super();
|
||||
this._accessibilityTreeRoot = document.createElement('div');
|
||||
this._accessibilityTreeRoot.classList.add('xterm-accessibility');
|
||||
|
||||
@@ -72,29 +71,28 @@ export class AccessibilityManager implements IDisposable {
|
||||
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this._disposables.push(this._renderRowsDebouncer);
|
||||
this._disposables.push(this._terminal.addDisposableListener('resize', data => this._onResize(data.cols, data.rows)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('refresh', data => this._refreshRows(data.start, data.end)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('scroll', data => this._refreshRows()));
|
||||
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._disposables.push(this._terminal.addDisposableListener('a11y.char', (char) => this._onChar(char)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('linefeed', () => this._onChar('\n')));
|
||||
this._disposables.push(this._terminal.addDisposableListener('a11y.tab', spaceCount => this._onTab(spaceCount)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('key', keyChar => this._onKey(keyChar)));
|
||||
this._disposables.push(this._terminal.addDisposableListener('blur', () => this._clearLiveRegion()));
|
||||
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._disposables.push(this._terminal.addDisposableListener('dprchange', () => this._refreshRowsDimensions()));
|
||||
this._disposables.push(this._terminal.renderer.addDisposableListener('resize', () => this._refreshRowsDimensions()));
|
||||
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._disposables.push(addDisposableListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._disposables.forEach(d => d.dispose());
|
||||
this._disposables.length = 0;
|
||||
super.dispose();
|
||||
this._terminal.element.removeChild(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
@@ -159,7 +157,7 @@ export class AccessibilityManager implements IDisposable {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(cols: number, rows: number): void {
|
||||
private _onResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
@@ -179,7 +177,7 @@ export class AccessibilityManager implements IDisposable {
|
||||
this._refreshRowsDimensions();
|
||||
}
|
||||
|
||||
public _createAccessibilityTreeNode(): HTMLElement {
|
||||
private _createAccessibilityTreeNode(): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.setAttribute('role', 'listitem');
|
||||
element.tabIndex = -1;
|
||||
@@ -261,6 +259,9 @@ export class AccessibilityManager implements IDisposable {
|
||||
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]);
|
||||
}
|
||||
|
||||
+345
-12
@@ -3,11 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { assert, expect } from 'chai';
|
||||
import { ITerminal } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { MockTerminal } from './utils/TestUtils.test';
|
||||
import { Buffer, DEFAULT_ATTR, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { MockTerminal, TestTerminal } 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;
|
||||
@@ -212,4 +270,279 @@ describe('Buffer', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
describe('stringIndexToBufferIndex', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({rows: 5, cols: 10});
|
||||
});
|
||||
|
||||
it('multiline ascii', () => {
|
||||
const input = 'This is ASCII text spanning multiple lines.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('combining e\u0301 in a sentence', () => {
|
||||
const input = 'Sitting in the cafe\u0301 drinking coffee.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < 19; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
}
|
||||
// string index 18 & 19 point to combining char e\u0301 ---> same buffer Index
|
||||
assert.deepEqual(
|
||||
terminal.buffer.stringIndexToBufferIndex(0, 18),
|
||||
terminal.buffer.stringIndexToBufferIndex(0, 19));
|
||||
// after the combining char every string index has an offset of -1
|
||||
for (let i = 19; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('multiline combining e\u0301', () => {
|
||||
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
// every buffer cell index contains 2 string indices
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('surrogate char in a sentence', () => {
|
||||
const input = 'The 𝄞 is a clef widely used in modern notation.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
}
|
||||
// string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index
|
||||
assert.deepEqual(
|
||||
terminal.buffer.stringIndexToBufferIndex(0, 4),
|
||||
terminal.buffer.stringIndexToBufferIndex(0, 5));
|
||||
// after the combining char every string index has an offset of -1
|
||||
for (let i = 5; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('multiline surrogate char', () => {
|
||||
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
// every buffer cell index contains 2 string indices
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('surrogate char with combining', () => {
|
||||
// eye of Ra with acute accent - string length of 3
|
||||
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
// index 0..2 should map to 0
|
||||
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1));
|
||||
assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2));
|
||||
for (let i = 2; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('multiline surrogate with combining', () => {
|
||||
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
// every buffer cell index contains 3 string indices
|
||||
for (let i = 0; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('fullwidth chars', () => {
|
||||
const input = 'These 123 are some fat numbers.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 0; i < 6; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex);
|
||||
}
|
||||
// string index 6, 7, 8 take 2 cells
|
||||
assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7));
|
||||
assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8));
|
||||
// rest of the string has offset of +3
|
||||
for (let i = 9; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('multiline fullwidth chars', () => {
|
||||
const input = '12345678901234567890';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
for (let i = 9; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i);
|
||||
assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
|
||||
it('fullwidth combining with emoji - match emoji cell', () => {
|
||||
const input = 'Lots of ¥\u0301 make me 😃.';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
const stringIndex = s.match(/😃/).index;
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex);
|
||||
assert(terminal.buffer.lines.get(bufferIndex[0]).get(bufferIndex[1])[CHAR_DATA_CHAR_INDEX], '😃');
|
||||
});
|
||||
|
||||
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
|
||||
const input = 'a12345678901234567890';
|
||||
// the 'a' at the beginning moves all fullwidth chars one to the right
|
||||
// now the end of the line contains a dangling empty cell since
|
||||
// the next fullwidth char has to wrap early
|
||||
// the dangling last cell is wrongly added in the string
|
||||
// --> fixable after resolving #1685
|
||||
terminal.writeSync(input);
|
||||
// TODO: reenable after fix
|
||||
// const s = terminal.buffer.contents(true).toArray()[0];
|
||||
// assert.equal(input, s);
|
||||
for (let i = 10; i < input.length; ++i) {
|
||||
const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i + 1); // TODO: remove +1 after fix
|
||||
const j = (i - 0) << 1;
|
||||
assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('BufferStringIterator', function(): void {
|
||||
it('iterator does not ovrflow buffer limits', function(): void {
|
||||
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
|
||||
const data = [
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaa\n',
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaa\n',
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaa\n',
|
||||
'aaaaaaaaaa',
|
||||
'aaaaaaaaaa'
|
||||
];
|
||||
terminal.writeSync(data.join(''));
|
||||
// brute force test with insane values
|
||||
expect(() => {
|
||||
for (let overscan = 0; overscan < 20; ++overscan) {
|
||||
for (let start = -10; start < 20; ++start) {
|
||||
for (let end = -10; end < 20; ++end) {
|
||||
const it = terminal.buffer.iterator(false, start, end, overscan, overscan);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}).to.not.throw();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+132
-21
@@ -3,17 +3,23 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { LineData, CharData, ITerminal, IBuffer } from './Types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { IDisposable, IMarker } from 'xterm';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { CharData, ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
|
||||
import { EventEmitter } from './common/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):
|
||||
@@ -22,7 +28,7 @@ export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
|
||||
* - scroll position
|
||||
*/
|
||||
export class Buffer implements IBuffer {
|
||||
public lines: CircularList<LineData>;
|
||||
public lines: CircularList<IBufferLine>;
|
||||
public ydisp: number;
|
||||
public ybase: number;
|
||||
public y: number;
|
||||
@@ -79,7 +85,7 @@ export class Buffer implements IBuffer {
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -92,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();
|
||||
@@ -116,7 +122,7 @@ export class Buffer implements IBuffer {
|
||||
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?
|
||||
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);
|
||||
@@ -141,7 +147,7 @@ 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +194,36 @@ export class Buffer implements IBuffer {
|
||||
this.scrollBottom = newRows - 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a string index back to a BufferIndex.
|
||||
* To get the correct buffer position the string must start at `startCol` 0
|
||||
* (default in translateBufferLineToString).
|
||||
* The method also works on wrapped line strings given rows were not trimmed.
|
||||
* The method operates on the CharData string length, there are no
|
||||
* additional content or boundary checks. Therefore the string and the buffer
|
||||
* should not be altered in between.
|
||||
* TODO: respect trim flag after fixing #1685
|
||||
* @param lineIndex line index the string was retrieved from
|
||||
* @param stringIndex index within the string
|
||||
* @param startCol column offset the string was retrieved from
|
||||
*/
|
||||
public stringIndexToBufferIndex(lineIndex: number, stringIndex: number): BufferIndex {
|
||||
while (stringIndex) {
|
||||
const line = this.lines.get(lineIndex);
|
||||
if (!line) {
|
||||
[-1, -1];
|
||||
}
|
||||
for (let i = 0; i < line.length; ++i) {
|
||||
stringIndex -= line.get(i)[CHAR_DATA_CHAR_INDEX].length;
|
||||
if (stringIndex < 0) {
|
||||
return [lineIndex, i];
|
||||
}
|
||||
}
|
||||
lineIndex++;
|
||||
}
|
||||
return [lineIndex, 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a buffer line to a string, with optional start and end columns.
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
@@ -218,7 +254,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
|
||||
@@ -226,7 +262,7 @@ export class Buffer implements IBuffer {
|
||||
if (startCol >= i) {
|
||||
startIndex--;
|
||||
}
|
||||
if (endCol >= i) {
|
||||
if (endCol > i) {
|
||||
endIndex--;
|
||||
}
|
||||
} else {
|
||||
@@ -259,12 +295,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);
|
||||
}
|
||||
@@ -283,7 +333,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);
|
||||
@@ -295,7 +345,7 @@ 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);
|
||||
@@ -305,14 +355,14 @@ export class Buffer implements IBuffer {
|
||||
public addMarker(y: number): Marker {
|
||||
const marker = new Marker(y);
|
||||
this.markers.push(marker);
|
||||
marker.disposables.push(this.lines.addDisposableListener('trim', amount => {
|
||||
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.on('dispose', () => this._removeMarker(marker));
|
||||
marker.register(marker.addDisposableListener('dispose', () => this._removeMarker(marker)));
|
||||
return marker;
|
||||
}
|
||||
|
||||
@@ -320,14 +370,17 @@ export class Buffer implements IBuffer {
|
||||
// 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);
|
||||
}
|
||||
|
||||
public iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator {
|
||||
return new BufferStringIterator(this, trimRight, startIndex, endIndex, startOverscan, endOverscan);
|
||||
}
|
||||
}
|
||||
|
||||
export class Marker extends EventEmitter implements IMarker {
|
||||
private static NEXT_ID = 1;
|
||||
private static _nextId = 1;
|
||||
|
||||
private _id: number = Marker.NEXT_ID++;
|
||||
private _id: number = Marker._nextId++;
|
||||
public isDisposed: boolean = false;
|
||||
public disposables: IDisposable[] = [];
|
||||
|
||||
public get id(): number { return this._id; }
|
||||
|
||||
@@ -342,8 +395,66 @@ export class Marker extends EventEmitter implements IMarker {
|
||||
return;
|
||||
}
|
||||
this.isDisposed = true;
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
this.disposables.length = 0;
|
||||
// Emit before super.dispose such that dispose listeners get a change to react
|
||||
this.emit('dispose');
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterator to get unwrapped content strings from the buffer.
|
||||
* The iterator returns at least the string data between the borders
|
||||
* `startIndex` and `endIndex` (exclusive) and will expand the lines
|
||||
* by `startOverscan` to the top and by `endOverscan` to the bottom,
|
||||
* if no new line was found in between.
|
||||
* It will never read/return string data beyond `startIndex - startOverscan`
|
||||
* or `endIndex + endOverscan`. Therefore the first and last line might be truncated.
|
||||
* It is possible to always get the full string for the first and last line as well
|
||||
* by setting the overscan values to the actual buffer length. This not recommended
|
||||
* since it might return the whole buffer within a single string in a worst case scenario.
|
||||
*/
|
||||
export class BufferStringIterator implements IBufferStringIterator {
|
||||
private _current: number;
|
||||
|
||||
constructor (
|
||||
private _buffer: IBuffer,
|
||||
private _trimRight: boolean,
|
||||
private _startIndex: number = 0,
|
||||
private _endIndex: number = _buffer.lines.length,
|
||||
private _startOverscan: number = 0,
|
||||
private _endOverscan: number = 0
|
||||
) {
|
||||
if (this._startIndex < 0) {
|
||||
this._startIndex = 0;
|
||||
}
|
||||
if (this._endIndex > this._buffer.lines.length) {
|
||||
this._endIndex = this._buffer.lines.length;
|
||||
}
|
||||
this._current = this._startIndex;
|
||||
}
|
||||
|
||||
public hasNext(): boolean {
|
||||
return this._current < this._endIndex;
|
||||
}
|
||||
|
||||
public next(): IBufferStringIteratorResult {
|
||||
const range = this._buffer.getWrappedRangeForLine(this._current);
|
||||
// limit search window to overscan value at both borders
|
||||
if (range.first < this._startIndex - this._startOverscan) {
|
||||
range.first = this._startIndex - this._startOverscan;
|
||||
}
|
||||
if (range.last > this._endIndex + this._endOverscan) {
|
||||
range.last = this._endIndex + this._endOverscan;
|
||||
}
|
||||
// limit to current buffer length
|
||||
range.first = Math.max(range.first, 0);
|
||||
range.last = Math.min(range.last, this._buffer.lines.length);
|
||||
let result = '';
|
||||
for (let i = range.first; i <= range.last; ++i) {
|
||||
// TODO: always apply trimRight after fixing #1685
|
||||
result += this._buffer.translateBufferLineToString(i, (this._trimRight) ? i === range.last : false);
|
||||
}
|
||||
this._current = range.last + 1;
|
||||
return {range: range, content: result};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -5,7 +5,7 @@
|
||||
|
||||
import { ITerminal, IBufferSet } from './Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
|
||||
/**
|
||||
* The BufferSet represents the set of two buffers used by xterm terminals (normal and alt) and
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TestTerminal } from './utils/TestUtils.test';
|
||||
import { assert } from 'chai';
|
||||
import { getStringCellWidth } from './CharWidth';
|
||||
import { IBuffer } from './Types';
|
||||
import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
|
||||
|
||||
|
||||
describe('getStringCellWidth', function(): void {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({rows: 5, cols: 30});
|
||||
});
|
||||
|
||||
function sumWidths(buffer: IBuffer, start: number, end: number, sentinel: string): number {
|
||||
let result = 0;
|
||||
for (let i = start; i < end; ++i) {
|
||||
const line = buffer.lines.get(i);
|
||||
for (let j = 0; j < line.length; ++j) { // TODO: change to trimBorder with multiline
|
||||
const ch = line.get(j);
|
||||
result += ch[CHAR_DATA_WIDTH_INDEX];
|
||||
// return on sentinel
|
||||
if (ch[CHAR_DATA_CHAR_INDEX] === sentinel) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
it('ASCII chars', function(): void {
|
||||
const input = 'This is just ASCII text.#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
it('combining chars', function(): void {
|
||||
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
it('surrogate chars', function(): void {
|
||||
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
it('surrogate combining chars', function(): void {
|
||||
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
it('fullwidth chars', function(): void {
|
||||
const input = '1234567890#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
it('fullwidth chars offset 1', function(): void {
|
||||
const input = 'a1234567890#';
|
||||
terminal.writeSync(input);
|
||||
const s = terminal.buffer.iterator(true).next().content;
|
||||
assert.equal(input, s);
|
||||
assert.equal(getStringCellWidth(s), sumWidths(terminal.buffer, 0, 1, '#'));
|
||||
});
|
||||
// TODO: multiline tests once #1685 is resolved
|
||||
});
|
||||
+24
-2
@@ -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;
|
||||
}
|
||||
@@ -169,3 +169,25 @@ export const wcwidth = (function(opts: {nul: number, control: number}): (ucs: nu
|
||||
return wcwidthHigh(num);
|
||||
};
|
||||
})({nul: 0, control: 0}); // configurable options
|
||||
|
||||
/**
|
||||
* Get the terminal cell width for a string.
|
||||
*/
|
||||
export function getStringCellWidth(s: string): number {
|
||||
let result = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
let code = s.charCodeAt(i);
|
||||
if (0xD800 <= code && code <= 0xDBFF) {
|
||||
const low = s.charCodeAt(i + 1);
|
||||
if (isNaN(low)) {
|
||||
return result;
|
||||
}
|
||||
code = ((code - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
|
||||
}
|
||||
if (0xDC00 <= code && code <= 0xDFFF) {
|
||||
continue;
|
||||
}
|
||||
result += wcwidth(code);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
|
||||
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 = {
|
||||
@@ -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();
|
||||
|
||||
@@ -61,7 +61,7 @@ export class CompositionHelper {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+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 ');
|
||||
});
|
||||
});
|
||||
|
||||
+690
-205
File diff suppressed because it is too large
Load Diff
+109
-10
@@ -4,16 +4,17 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { IMouseZoneManager, IMouseZone } from './input/Types';
|
||||
import { ILinkMatcher, LineData, ITerminal } from './Types';
|
||||
import { IMouseZoneManager, IMouseZone } from './ui/Types';
|
||||
import { ILinkMatcher, ITerminal, IBufferLine } from './Types';
|
||||
import { Linkifier } from './Linkifier';
|
||||
import { MockBuffer, MockTerminal } from './utils/TestUtils.test';
|
||||
import { CircularList } from './utils/CircularList';
|
||||
import { MockBuffer, MockTerminal, TestTerminal } from './utils/TestUtils.test';
|
||||
import { CircularList } from './common/CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
|
||||
class TestLinkifier extends Linkifier {
|
||||
constructor(_terminal: ITerminal) {
|
||||
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 {
|
||||
@@ -39,15 +42,16 @@ describe('Linkifier', () => {
|
||||
beforeEach(() => {
|
||||
terminal = new MockTerminal();
|
||||
terminal.cols = 100;
|
||||
terminal.rows = 10;
|
||||
terminal.buffer = new MockBuffer();
|
||||
(<MockBuffer>terminal.buffer).setLines(new CircularList<LineData>(20));
|
||||
(<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)]);
|
||||
}
|
||||
@@ -61,6 +65,7 @@ describe('Linkifier', () => {
|
||||
function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: MochaDone): void {
|
||||
addRow(rowText);
|
||||
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
|
||||
terminal.rows = terminal.buffer.lines.length - 1;
|
||||
linkifier.linkifyRows();
|
||||
// Allow linkify to happen
|
||||
setTimeout(() => {
|
||||
@@ -235,4 +240,98 @@ describe('Linkifier', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('unicode handling', () => {
|
||||
let terminal: TestTerminal;
|
||||
|
||||
// other than the tests above unicode testing needs the full terminal instance
|
||||
// to get the special handling of fullwidth, surrogate and combining chars in the input handler
|
||||
beforeEach(() => {
|
||||
terminal = new TestTerminal({cols: 10, rows: 5});
|
||||
linkifier = new TestLinkifier(terminal);
|
||||
mouseZoneManager = new TestMouseZoneManager();
|
||||
linkifier.attachToDom(mouseZoneManager);
|
||||
});
|
||||
|
||||
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: MochaDone): void {
|
||||
terminal.writeSync(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);
|
||||
}
|
||||
|
||||
describe('unicode before the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
describe('unicode within the match', () => {
|
||||
it('combining - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining surrogate - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining surrogate - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
it('combining fullwidth - match within one line', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
|
||||
});
|
||||
it('combining fullwidth - match over two lines', function(done: () => void): void {
|
||||
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+104
-86
@@ -3,10 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IMouseZoneManager } from './input/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal } from './Types';
|
||||
import { MouseZone } from './input/MouseZoneManager';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { IMouseZoneManager } from './ui/Types';
|
||||
import { ILinkHoverEvent, ILinkMatcher, LinkMatcherHandler, LinkHoverEventTypes, ILinkMatcherOptions, ILinkifier, ITerminal, IBufferStringIteratorResult } from './Types';
|
||||
import { MouseZone } from './ui/MouseZoneManager';
|
||||
import { EventEmitter } from './common/EventEmitter';
|
||||
import { CHAR_DATA_ATTR_INDEX } from './Buffer';
|
||||
import { getStringCellWidth } from './CharWidth';
|
||||
|
||||
/**
|
||||
* The Linkifier applies links to rows shortly after they have been refreshed.
|
||||
@@ -17,14 +19,21 @@ 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;
|
||||
|
||||
/**
|
||||
* Limit of the unwrapping line expansion (overscan) at the top and bottom
|
||||
* of the actual viewport in ASCII characters.
|
||||
* A limit of 2000 should match most sane urls.
|
||||
*/
|
||||
protected static readonly OVERSCAN_CHAR_LIMIT = 2000;
|
||||
|
||||
protected _linkMatchers: ILinkMatcher[] = [];
|
||||
|
||||
private _mouseZoneManager: IMouseZoneManager;
|
||||
private _rowsTimeoutId: number;
|
||||
private _nextLinkMatcherId = 0;
|
||||
private _rowsToLinkify: {start: number, end: number};
|
||||
private _rowsToLinkify: { start: number, end: number };
|
||||
|
||||
constructor(
|
||||
protected _terminal: ITerminal
|
||||
@@ -79,9 +88,37 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
*/
|
||||
private _linkifyRows(): void {
|
||||
this._rowsTimeoutId = null;
|
||||
for (let i = this._rowsToLinkify.start; i <= this._rowsToLinkify.end; i++) {
|
||||
this._linkifyRow(i);
|
||||
const buffer = this._terminal.buffer;
|
||||
|
||||
// Ensure the start row exists
|
||||
const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start;
|
||||
if (absoluteRowIndexStart >= buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Invalidate bad end row values (if a resize happened)
|
||||
const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._terminal.rows) + 1;
|
||||
|
||||
// Iterate over the range of unwrapped content strings within start..end
|
||||
// (excluding).
|
||||
// _doLinkifyRow gets full unwrapped lines with the start row as buffer offset
|
||||
// for every matcher.
|
||||
// The unwrapping is needed to also match content that got wrapped across
|
||||
// several buffer lines. To avoid a worst case scenario where the whole buffer
|
||||
// contains just a single unwrapped string we limit this line expansion beyond
|
||||
// the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom.
|
||||
// This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT
|
||||
// chars will not match anymore at the viewport borders.
|
||||
const overscanLineLimit = Math.ceil(Linkifier.OVERSCAN_CHAR_LIMIT / this._terminal.cols);
|
||||
const iterator = this._terminal.buffer.iterator(
|
||||
false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit);
|
||||
while (iterator.hasNext()) {
|
||||
const lineData: IBufferStringIteratorResult = iterator.next();
|
||||
for (let i = 0; i < this._linkMatchers.length; i++) {
|
||||
this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]);
|
||||
}
|
||||
}
|
||||
|
||||
this._rowsToLinkify.start = null;
|
||||
this._rowsToLinkify.end = null;
|
||||
}
|
||||
@@ -152,82 +189,61 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row.
|
||||
* @param rowIndex The index of the row to linkify.
|
||||
*/
|
||||
private _linkifyRow(rowIndex: number): void {
|
||||
// Ensure the row exists
|
||||
let absoluteRowIndex = this._terminal.buffer.ydisp + rowIndex;
|
||||
if (absoluteRowIndex >= this._terminal.buffer.lines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ((<any>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
|
||||
do {
|
||||
rowIndex--;
|
||||
absoluteRowIndex--;
|
||||
} while ((<any>this._terminal.buffer.lines.get(absoluteRowIndex)).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 &&
|
||||
(<any>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]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Linkifies a row given a specific handler.
|
||||
* @param rowIndex The row index to linkify.
|
||||
* @param text The text of the row (excludes text in the row that's already
|
||||
* linkified).
|
||||
* @param rowIndex The row index to linkify (absolute index).
|
||||
* @param text string content of the unwrapped row.
|
||||
* @param matcher The link matcher for this line.
|
||||
* @param offset The how much of the row has already been linkified.
|
||||
* @return The link element(s) that were added.
|
||||
*/
|
||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher, offset: number = 0): void {
|
||||
// Find the first match
|
||||
let match = text.match(matcher.regex);
|
||||
if (!match || match.length === 0) {
|
||||
return;
|
||||
}
|
||||
let 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);
|
||||
|
||||
// Ensure the link is valid before registering
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
// Discard link if the line has already changed
|
||||
if (this._rowsTimeoutId) {
|
||||
return;
|
||||
private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void {
|
||||
// clone regex to do a global search on text
|
||||
const rex = new RegExp(matcher.regex.source, matcher.regex.flags + 'g');
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
while ((match = rex.exec(text)) !== null) {
|
||||
const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex];
|
||||
if (!uri) {
|
||||
// something matched but does not comply with the given matchIndex
|
||||
// since this is most likely a bug the regex itself we simply do nothing here
|
||||
// DEBUG: print match and throw
|
||||
if ((<any>this._terminal).debug) {
|
||||
console.log({match, matcher});
|
||||
throw new Error('match found without corresponding matchIndex');
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(offset + index, rowIndex, uri, matcher);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Recursively check for links in the rest of the text
|
||||
const remainingStartIndex = index + uri.length;
|
||||
const remainingText = text.substr(remainingStartIndex);
|
||||
if (remainingText.length > 0) {
|
||||
this._doLinkifyRow(rowIndex, remainingText, matcher, offset + remainingStartIndex);
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
// therefore we cannot use match.index directly, instead we search the position
|
||||
// of the match group in text again
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = text.indexOf(uri, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + uri.length;
|
||||
|
||||
// get the buffer index as [absolute row, col] for the match
|
||||
const bufferIndex = this._terminal.buffer.stringIndexToBufferIndex(rowIndex, stringIndex);
|
||||
|
||||
const line = this._terminal.buffer.lines.get(bufferIndex[0]);
|
||||
const char = line.get(bufferIndex[1]);
|
||||
let fg: number | undefined;
|
||||
if (char) {
|
||||
const attr: number = char[CHAR_DATA_ATTR_INDEX];
|
||||
fg = (attr >> 9) & 0x1ff;
|
||||
}
|
||||
|
||||
if (matcher.validationCallback) {
|
||||
matcher.validationCallback(uri, isValid => {
|
||||
// Discard link if the line has already changed
|
||||
if (this._rowsTimeoutId) {
|
||||
return;
|
||||
}
|
||||
if (isValid) {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this._addLink(bufferIndex[1], bufferIndex[0] - this._terminal.buffer.ydisp, uri, matcher, fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +253,14 @@ 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 width = getStringCellWidth(uri);
|
||||
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);
|
||||
let x2 = (x1 + width) % this._terminal.cols;
|
||||
let y2 = y1 + Math.floor((x1 + width) / this._terminal.cols);
|
||||
if (x2 === 0) {
|
||||
x2 = this._terminal.cols;
|
||||
y2--;
|
||||
@@ -260,17 +278,17 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
window.open(uri, '_blank');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this.emit(LinkHoverEventTypes.HOVER, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.add('xterm-cursor-pointer');
|
||||
},
|
||||
e => {
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this.emit(LinkHoverEventTypes.TOOLTIP, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
if (matcher.hoverTooltipCallback) {
|
||||
matcher.hoverTooltipCallback(e, uri);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2));
|
||||
this.emit(LinkHoverEventTypes.LEAVE, this._createLinkHoverEvent(x1, y1, x2, y2, fg));
|
||||
this._terminal.element.classList.remove('xterm-cursor-pointer');
|
||||
if (matcher.hoverLeaveCallback) {
|
||||
matcher.hoverLeaveCallback();
|
||||
@@ -285,7 +303,7 @@ export class Linkifier extends EventEmitter implements ILinkifier {
|
||||
));
|
||||
}
|
||||
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols };
|
||||
private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number): ILinkHoverEvent {
|
||||
return { x1, y1, x2, y2, cols: this._terminal.cols, fg };
|
||||
}
|
||||
}
|
||||
|
||||
-673
@@ -1,673 +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, IInputHandlingTerminal } 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 const 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 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':
|
||||
(<IInputHandlingTerminal>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--;
|
||||
// }
|
||||
}
|
||||
@@ -4,12 +4,13 @@
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
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, 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 {}
|
||||
@@ -25,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); }
|
||||
|
||||
@@ -50,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', () => {
|
||||
@@ -95,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)],
|
||||
@@ -111,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, '中文');
|
||||
@@ -181,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
|
||||
@@ -298,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', () => {
|
||||
@@ -368,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+161
-59
@@ -3,13 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, XtermListener } from './Types';
|
||||
import { ITerminal, ISelectionManager, IBuffer, CharData, IBufferLine } from './Types';
|
||||
import { XtermListener } from './common/Types';
|
||||
import { MouseHelper } from './utils/MouseHelper';
|
||||
import * as Browser from './shared/utils/Browser';
|
||||
import { CharMeasure } from './utils/CharMeasure';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { CharMeasure } from './ui/CharMeasure';
|
||||
import { EventEmitter } from './common/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';
|
||||
|
||||
/**
|
||||
@@ -54,10 +55,11 @@ interface IWordPosition {
|
||||
/**
|
||||
* A selection mode, this drives how the selection behaves on mouse move.
|
||||
*/
|
||||
const enum SelectionMode {
|
||||
export const enum SelectionMode {
|
||||
NORMAL,
|
||||
WORD,
|
||||
LINE
|
||||
LINE,
|
||||
COLUMN
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +82,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
|
||||
@@ -116,6 +118,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;
|
||||
}
|
||||
@@ -177,30 +184,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,7 +269,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
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -358,7 +382,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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -424,8 +452,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;
|
||||
}
|
||||
@@ -449,7 +479,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);
|
||||
@@ -471,7 +501,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]++;
|
||||
}
|
||||
@@ -502,6 +532,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.
|
||||
@@ -539,18 +577,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]++;
|
||||
}
|
||||
@@ -572,10 +613,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();
|
||||
}
|
||||
@@ -586,7 +636,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();
|
||||
|
||||
@@ -612,10 +662,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.
|
||||
@@ -642,7 +692,7 @@ 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;
|
||||
@@ -684,24 +734,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++;
|
||||
@@ -715,8 +765,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++;
|
||||
@@ -737,7 +787,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
|
||||
@@ -745,7 +795,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
|
||||
@@ -757,6 +807,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 };
|
||||
}
|
||||
|
||||
@@ -768,6 +846,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;
|
||||
}
|
||||
@@ -780,7 +863,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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -803,7 +903,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,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
|
||||
|
||||
+27
-22
@@ -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,21 +124,22 @@ 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(){};
|
||||
|
||||
// Perform a synchronous .write(data)
|
||||
xterm.writeBuffer.push(fromPty);
|
||||
xterm._innerWrite();
|
||||
xterm.innerWrite();
|
||||
|
||||
const fromEmulator = terminalToString(xterm);
|
||||
console.log = consoleLog;
|
||||
const expected = fs.readFileSync(filename.split('.')[0] + '.text', 'utf8');
|
||||
|
||||
let fromEmulator = terminalToString(xterm);
|
||||
console.log = CONSOLE_LOG;
|
||||
let 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'));
|
||||
});
|
||||
});
|
||||
|
||||
+300
-425
File diff suppressed because it is too large
Load Diff
+301
-673
File diff suppressed because it is too large
Load Diff
+216
-48
@@ -3,20 +3,25 @@
|
||||
* @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';
|
||||
import { ICircularList } from './common/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) => void;
|
||||
export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
|
||||
|
||||
export type CharacterJoinerHandler = (text: string) => [number, number][];
|
||||
|
||||
// BufferIndex denotes a position in the buffer: [rowIndex, colIndex]
|
||||
export type BufferIndex = [number, number];
|
||||
|
||||
export const enum LinkHoverEventTypes {
|
||||
HOVER = 'linkhover',
|
||||
TOOLTIP = 'linktooltip',
|
||||
@@ -43,9 +48,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;
|
||||
@@ -64,17 +68,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;
|
||||
@@ -85,9 +83,13 @@ export interface IInputHandlingTerminal extends IEventEmitter {
|
||||
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;
|
||||
@@ -106,10 +108,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;
|
||||
@@ -135,26 +138,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 {
|
||||
@@ -169,16 +195,13 @@ export interface ILinkMatcher {
|
||||
willLinkActivate?: (event: MouseEvent, uri: string) => boolean;
|
||||
}
|
||||
|
||||
export interface ICharset {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface ILinkHoverEvent {
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
cols: number;
|
||||
fg: number;
|
||||
}
|
||||
|
||||
export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
|
||||
@@ -190,7 +213,6 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
|
||||
writeBuffer: string[];
|
||||
cursorHidden: boolean;
|
||||
cursorState: number;
|
||||
defAttr: number;
|
||||
options: ITerminalOptions;
|
||||
buffer: IBuffer;
|
||||
buffers: IBufferSet;
|
||||
@@ -205,12 +227,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 {
|
||||
@@ -226,7 +246,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 };
|
||||
}
|
||||
|
||||
@@ -238,6 +258,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;
|
||||
@@ -247,8 +268,18 @@ export interface ITerminalOptions extends IPublicTerminalOptions {
|
||||
useFlowControl?: boolean;
|
||||
}
|
||||
|
||||
export interface IBufferStringIteratorResult {
|
||||
range: {first: number, last: number};
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface IBufferStringIterator {
|
||||
hasNext(): boolean;
|
||||
next(): IBufferStringIteratorResult;
|
||||
}
|
||||
|
||||
export interface IBuffer {
|
||||
readonly lines: ICircularList<LineData>;
|
||||
readonly lines: ICircularList<IBufferLine>;
|
||||
ydisp: number;
|
||||
ybase: number;
|
||||
y: number;
|
||||
@@ -261,8 +292,11 @@ export interface IBuffer {
|
||||
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;
|
||||
stringIndexToBufferIndex(lineIndex: number, stringIndex: number): number[];
|
||||
iterator(trimRight: boolean, startIndex?: number, endIndex?: number, startOverscan?: number, endOverscan?: number): IBufferStringIterator;
|
||||
}
|
||||
|
||||
export interface IBufferSet extends IEventEmitter {
|
||||
@@ -274,19 +308,6 @@ export interface IBufferSet extends IEventEmitter {
|
||||
activateAltBuffer(): void;
|
||||
}
|
||||
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
|
||||
get(index: number): T;
|
||||
set(index: number, value: T): void;
|
||||
push(value: T): void;
|
||||
pop(): T;
|
||||
splice(start: number, deleteCount: number, ...items: T[]): void;
|
||||
trimStart(count: number): void;
|
||||
shiftElements(start: number, count: number, offset: number): void;
|
||||
}
|
||||
|
||||
export interface ISelectionManager {
|
||||
selectionText: string;
|
||||
selectionStart: [number, number];
|
||||
@@ -355,3 +376,150 @@ export interface IBrowser {
|
||||
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;
|
||||
}
|
||||
|
||||
+64
-18
@@ -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,19 +15,23 @@ 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 _lastScrollTop: number = 0;
|
||||
|
||||
// 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.
|
||||
@@ -39,11 +45,13 @@ export class Viewport implements IViewport {
|
||||
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.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
|
||||
// Perform this async to ensure the CharMeasure is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
@@ -58,6 +66,12 @@ export class Viewport implements IViewport {
|
||||
* necessary.
|
||||
*/
|
||||
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;
|
||||
@@ -67,30 +81,53 @@ export class Viewport implements IViewport {
|
||||
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 buffer height changed
|
||||
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) {
|
||||
// If viewport height changed
|
||||
this._refresh();
|
||||
} else {
|
||||
// If size has changed, refresh viewport
|
||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Sync scrollTop
|
||||
const scrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._viewportElement.scrollTop !== scrollTop) {
|
||||
this._viewportElement.scrollTop = scrollTop;
|
||||
// If viewport height changed
|
||||
if (this._lastRecordedViewportHeight !== (<any>this._terminal).renderer.dimensions.canvasHeight) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// If the buffer position doesn't match last scroll top
|
||||
const newScrollTop = this._terminal.buffer.ydisp * this._currentRowHeight;
|
||||
if (this._lastScrollTop !== newScrollTop) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// If element's scroll top changed, this can happen when hiding the element
|
||||
if (this._lastScrollTop !== this._viewportElement.scrollTop) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// If row height changed
|
||||
if (this._terminal.renderer.dimensions.scaledCellHeight / window.devicePixelRatio !== this._currentRowHeight) {
|
||||
this._refresh();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,13 +137,22 @@ export class Viewport implements IViewport {
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private _onScroll(ev: Event): void {
|
||||
// Record current scroll top position
|
||||
this._lastScrollTop = this._viewportElement.scrollTop;
|
||||
|
||||
// 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRow = Math.round(this._viewportElement.scrollTop / this._currentRowHeight);
|
||||
// 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._lastScrollTop / this._currentRowHeight);
|
||||
const diff = newRow - this._terminal.buffer.ydisp;
|
||||
this._terminal.scrollLines(diff, true);
|
||||
}
|
||||
@@ -180,7 +226,7 @@ export class Viewport implements IViewport {
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent): void {
|
||||
let deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
const deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
return;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
+24
-11
@@ -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,10 +36,11 @@ 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 (!myTextDecoder) {
|
||||
@@ -51,10 +50,10 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean
|
||||
str = myTextDecoder.decode(ev.data);
|
||||
displayData(str);
|
||||
} else {
|
||||
let fileReader = new FileReader();
|
||||
const fileReader = new FileReader();
|
||||
|
||||
fileReader.addEventListener('load', () => {
|
||||
str = myTextDecoder.decode(this.result);
|
||||
str = myTextDecoder.decode(fileReader.result);
|
||||
displayData(str);
|
||||
});
|
||||
fileReader.readAsArrayBuffer(ev.data);
|
||||
@@ -88,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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/fullscreen/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,15 +5,30 @@
|
||||
|
||||
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 ISearchHelper {
|
||||
findNext(term: string): boolean;
|
||||
findPrevious(term: string): boolean;
|
||||
export interface ISearchAddonTerminal extends Terminal {
|
||||
__searchHelper?: ISearchHelper;
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
|
||||
export interface ISearchHelper {
|
||||
findNext(term: string, searchOptions: ISearchOptions): boolean;
|
||||
findPrevious(term: string, searchOptions: ISearchOptions): boolean;
|
||||
}
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
wholeWord?: boolean;
|
||||
caseSensitive?: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
row: number;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ISearchHelper, ISearchAddonTerminal } from './Interfaces';
|
||||
|
||||
interface ISearchResult {
|
||||
term: string;
|
||||
col: number;
|
||||
row: number;
|
||||
}
|
||||
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
|
||||
const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?';
|
||||
|
||||
/**
|
||||
* A class that knows how to search the terminal and how to display the results.
|
||||
@@ -19,31 +14,31 @@ export class SearchHelper implements ISearchHelper {
|
||||
// TODO: Search for multiple instances on 1 line
|
||||
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
|
||||
// TODO: Highlight other instances in the viewport
|
||||
// TODO: Support regex, case sensitivity, etc.
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string): boolean {
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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++) {
|
||||
result = this._findInLine(term, y);
|
||||
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -52,7 +47,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
// Search from the top to the current ydisp
|
||||
if (!result) {
|
||||
for (let y = 0; y < startRow; y++) {
|
||||
result = this._findInLine(term, y);
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -67,24 +62,25 @@ export class SearchHelper implements ISearchHelper {
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string): boolean {
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!term || term.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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
|
||||
for (let y = startRow - 1; y >= 0; y--) {
|
||||
result = this._findInLine(term, y);
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -92,8 +88,8 @@ 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--) {
|
||||
result = this._findInLine(term, y);
|
||||
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
|
||||
result = this._findInLine(term, y, searchOptions);
|
||||
if (result) {
|
||||
break;
|
||||
}
|
||||
@@ -105,19 +101,61 @@ export class SearchHelper implements ISearchHelper {
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a line for a search term.
|
||||
* A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it.
|
||||
* @param searchIndex starting indext of the potential whole word substring
|
||||
* @param line entire string in which the potential whole word was found
|
||||
* @param term the substring that starts at searchIndex
|
||||
*/
|
||||
private _isWholeWord(searchIndex: number, line: string, term: string): boolean {
|
||||
return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) &&
|
||||
(((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
|
||||
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
|
||||
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||
* text starts on is searched.
|
||||
* @param term Tne search term.
|
||||
* @param y The line to search.
|
||||
* @param searchOptions Search options.
|
||||
* @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 lowerTerm = term.toLowerCase();
|
||||
let searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
|
||||
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
|
||||
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
|
||||
let searchIndex = -1;
|
||||
|
||||
if (searchOptions.regex) {
|
||||
const searchRegex = RegExp(searchTerm, 'g');
|
||||
const foundTerm = searchRegex.exec(searchStringLine);
|
||||
if (foundTerm && foundTerm[0].length > 0) {
|
||||
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||
term = foundTerm[0];
|
||||
}
|
||||
} else {
|
||||
searchIndex = searchStringLine.indexOf(searchTerm);
|
||||
}
|
||||
|
||||
if (searchIndex >= 0) {
|
||||
const line = this._terminal.buffer.lines.get(y);
|
||||
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
|
||||
if (searchIndex >= this._terminal.cols) {
|
||||
y += Math.floor(searchIndex / this._terminal.cols);
|
||||
searchIndex = searchIndex % this._terminal.cols;
|
||||
}
|
||||
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -138,6 +176,28 @@ export class SearchHelper implements ISearchHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Translates a buffer line to a string, including subsequent lines if they are wraps.
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
* function is useful for getting the actual text underneath the raw selection
|
||||
* position.
|
||||
* @param line The line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
*/
|
||||
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
|
||||
let lineString = '';
|
||||
let lineWrapsToNext: boolean;
|
||||
|
||||
do {
|
||||
const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1);
|
||||
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight);
|
||||
lineIndex++;
|
||||
} while (lineWrapsToNext);
|
||||
|
||||
return lineString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
@@ -147,8 +207,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,248 @@
|
||||
/**
|
||||
* 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 { ISearchOptions, ISearchResult } from './Interfaces';
|
||||
|
||||
|
||||
class MockTerminalPlain {}
|
||||
|
||||
class MockTerminal {
|
||||
private _core: any;
|
||||
public searchHelper: TestSearchHelper;
|
||||
public cols: number;
|
||||
constructor(options: any) {
|
||||
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
||||
this.searchHelper = new TestSearchHelper(this as any);
|
||||
this.cols = options.cols;
|
||||
}
|
||||
get core(): any {
|
||||
return this._core;
|
||||
}
|
||||
pushWriteData(): void {
|
||||
this._core._innerWrite();
|
||||
}
|
||||
}
|
||||
|
||||
class TestSearchHelper extends SearchHelper {
|
||||
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
|
||||
return this._findInLine(term, y, searchOptions);
|
||||
}
|
||||
}
|
||||
|
||||
describe('search addon', () => {
|
||||
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');
|
||||
});
|
||||
});
|
||||
describe('find', () => {
|
||||
it('Searchhelper - should find correct position', () => {
|
||||
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.findInLine('Hello', 0);
|
||||
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||
const hello2 = term.searchHelper.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'});
|
||||
});
|
||||
it('should find search term accross line wrap', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 10, rows: 5});
|
||||
term.core.write('texttextHellotext\r\n');
|
||||
term.core.write('texttexttextHellotext goodbye');
|
||||
term.pushWriteData();
|
||||
/*
|
||||
texttextHe
|
||||
llotext
|
||||
texttextte
|
||||
xtHellotex
|
||||
t (these spaces included intentionally)
|
||||
goodbye
|
||||
*/
|
||||
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||
const hello3 = term.searchHelper.findInLine('Hello', 3);
|
||||
const llo = term.searchHelper.findInLine('llo', 1);
|
||||
const goodbye = term.searchHelper.findInLine('goodbye', 2);
|
||||
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql(undefined);
|
||||
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
|
||||
expect(hello3).eql(undefined);
|
||||
expect(llo).eql(undefined);
|
||||
expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'});
|
||||
});
|
||||
it('should respect search regex', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 10, rows: 4});
|
||||
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
|
||||
/*
|
||||
abcdefghij
|
||||
klmnopqrst
|
||||
uvwxyz
|
||||
~/dev
|
||||
*/
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: true,
|
||||
wholeWord: false,
|
||||
caseSensitive: false
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
|
||||
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
|
||||
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
|
||||
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
|
||||
expect(hello0).eql({col: 3, row: 0, term: 'de'});
|
||||
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
|
||||
expect(hello2).eql(undefined);
|
||||
expect(tilda0).eql({col: 0, row: 3, term: '~'});
|
||||
expect(tilda1).eql({col: 0, row: 3, term: '~'});
|
||||
expect(tilda2).eql({col: 0, row: 3, term: '~'});
|
||||
});
|
||||
it('should not select empty lines', () => {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 3});
|
||||
term.core.write(' ');
|
||||
term.pushWriteData();
|
||||
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||
expect(line).eql(undefined);
|
||||
});
|
||||
it('should respect case sensitive', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 4});
|
||||
term.core.write('Hello World\r\n123....hello\r\nmoreTestHello');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: false,
|
||||
wholeWord: false,
|
||||
caseSensitive: true
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql(undefined);
|
||||
expect(hello2).eql({col: 8, row: 2, term: 'Hello'});
|
||||
});
|
||||
it('should respect case sensitive + regex', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 4});
|
||||
term.core.write('hellohello\r\nHelloHello');
|
||||
term.pushWriteData();
|
||||
|
||||
/**
|
||||
* hellohello
|
||||
* HelloHello
|
||||
*/
|
||||
|
||||
const searchOptions = {
|
||||
regex: true,
|
||||
wholeWord: false,
|
||||
caseSensitive: true
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||
expect(hello0).eql(undefined);
|
||||
expect(hello1).eql(undefined);
|
||||
expect(hello2).eql({col: 0, row: 1, term: 'Hello'});
|
||||
expect(hello3).eql({col: 5, row: 1, term: 'Hello'});
|
||||
});
|
||||
it('should respect whole-word search option', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: false,
|
||||
wholeWord: true,
|
||||
caseSensitive: false
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 2, searchOptions);
|
||||
const hello3 = term.searchHelper.findInLine('Hello', 3, searchOptions);
|
||||
const hello4 = term.searchHelper.findInLine('Hello', 4, searchOptions);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql({col: 6, row: 1, term: 'Hello'});
|
||||
expect(hello2).eql(undefined);
|
||||
expect(hello3).eql(undefined);
|
||||
expect(hello4).eql(undefined);
|
||||
});
|
||||
it('should respect whole-word + case sensitive search options', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('Hello World\r\nHelloWorld');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: false,
|
||||
wholeWord: true,
|
||||
caseSensitive: true
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||
const hello3 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql(undefined);
|
||||
expect(hello2).eql(undefined);
|
||||
expect(hello3).eql(undefined);
|
||||
});
|
||||
it('should respect whole-word + regex search options', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: true,
|
||||
wholeWord: true,
|
||||
caseSensitive: false
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('Hello', 1, searchOptions);
|
||||
const hello3 = term.searchHelper.findInLine('Hello$', 1, searchOptions);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'hello'});
|
||||
expect(hello1).eql({col: 12, row: 0, term: 'hello'});
|
||||
expect(hello2).eql(undefined);
|
||||
expect(hello3).eql(undefined);
|
||||
});
|
||||
it('should respect all search options', function(): void {
|
||||
search.apply(<any>MockTerminal);
|
||||
const term = new MockTerminal({cols: 20, rows: 5});
|
||||
term.core.write('Hello World Hello\r\nHelloWorldHello');
|
||||
term.pushWriteData();
|
||||
const searchOptions = {
|
||||
regex: true,
|
||||
wholeWord: true,
|
||||
caseSensitive: true
|
||||
};
|
||||
const hello0 = term.searchHelper.findInLine('Hello', 0, searchOptions);
|
||||
const hello1 = term.searchHelper.findInLine('Hello$', 0, searchOptions);
|
||||
const hello2 = term.searchHelper.findInLine('hello', 0, searchOptions);
|
||||
const hello3 = term.searchHelper.findInLine('hello$', 0, searchOptions);
|
||||
const hello4 = term.searchHelper.findInLine('hello', 1, searchOptions);
|
||||
const hello5 = term.searchHelper.findInLine('hello$', 1, searchOptions);
|
||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||
expect(hello1).eql({col: 12, row: 0, term: 'Hello'});
|
||||
expect(hello2).eql(undefined);
|
||||
expect(hello3).eql(undefined);
|
||||
expect(hello4).eql(undefined);
|
||||
expect(hello5).eql(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
+11
-11
@@ -3,46 +3,46 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { SearchHelper } from './SearchHelper';
|
||||
import { Terminal } from 'xterm';
|
||||
import { ISearchAddonTerminal } from './Interfaces';
|
||||
import { ISearchAddonTerminal, ISearchOptions } from './Interfaces';
|
||||
|
||||
/**
|
||||
* Find the next instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @param searchOptions Search options
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
export function findNext(terminal: Terminal, term: string): boolean {
|
||||
export function findNext(terminal: Terminal, term: string, searchOptions: ISearchOptions = {}): boolean {
|
||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findNext(term);
|
||||
return addonTerminal.__searchHelper.findNext(term, searchOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the previous instance of the term, then scroll to and select it. If it
|
||||
* doesn't exist, do nothing.
|
||||
* @param term Tne search term.
|
||||
* @param searchOptions Search options
|
||||
* @return Whether a result was found.
|
||||
*/
|
||||
export function findPrevious(terminal: Terminal, term: string): boolean {
|
||||
export function findPrevious(terminal: Terminal, term: string, searchOptions: ISearchOptions): boolean {
|
||||
const addonTerminal = <ISearchAddonTerminal>terminal;
|
||||
if (!addonTerminal.__searchHelper) {
|
||||
addonTerminal.__searchHelper = new SearchHelper(addonTerminal);
|
||||
}
|
||||
return addonTerminal.__searchHelper.findPrevious(term);
|
||||
return addonTerminal.__searchHelper.findPrevious(term, searchOptions);
|
||||
}
|
||||
|
||||
export function apply(terminalConstructor: typeof Terminal): void {
|
||||
(<any>terminalConstructor.prototype).findNext = function(term: string): boolean {
|
||||
return findNext(this, term);
|
||||
(<any>terminalConstructor.prototype).findNext = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||
return findNext(this, term, searchOptions);
|
||||
};
|
||||
|
||||
(<any>terminalConstructor.prototype).findPrevious = function(term: string): boolean {
|
||||
return findPrevious(this, term);
|
||||
(<any>terminalConstructor.prototype).findPrevious = function(term: string, searchOptions: ISearchOptions): boolean {
|
||||
return findPrevious(this, term, searchOptions);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"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';
|
||||
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/terminado/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"outDir": "../../../lib/addons/webLinks/",
|
||||
"sourceMap": true,
|
||||
"removeComments": true,
|
||||
"declaration": true
|
||||
}
|
||||
"declaration": true,
|
||||
"preserveWatchOutput": true
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"../../../typings/xterm.d.ts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ describe('webLinks addon', () => {
|
||||
|
||||
const row = ' http://foo.com/a~b#c~d?e~f ';
|
||||
|
||||
let match = row.match(term.regex);
|
||||
let uri = match[term.options.matchIndex];
|
||||
const match = row.match(term.regex);
|
||||
const uri = match[term.options.matchIndex];
|
||||
|
||||
assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
|
||||
});
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/// <reference path="../../../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal, ILinkMatcherOptions } from 'xterm';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,10 +2,19 @@
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"es6",
|
||||
],
|
||||
"rootDir": ".",
|
||||
"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';
|
||||
|
||||
/**
|
||||
@@ -48,13 +46,13 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
|
||||
|
||||
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)
|
||||
);
|
||||
@@ -72,7 +70,7 @@ function zmodemAttach(ws: WebSocket, opts: IZmodemOptions = {}): void {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { ICircularList } from '../Types';
|
||||
import { EventEmitter } from './EventEmitter';
|
||||
import { ICircularList } from './Types';
|
||||
|
||||
/**
|
||||
* Represents a circular list; a list with a maximum size that wraps around when push is called,
|
||||
* overriding values at the start of the list.
|
||||
*/
|
||||
export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
protected _array: T[];
|
||||
protected _array: (T | undefined)[];
|
||||
private _startIndex: number;
|
||||
private _length: number;
|
||||
|
||||
@@ -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 | undefined>(newMaxLength);
|
||||
for (let i = 0; i < Math.min(newMaxLength, this.length); i++) {
|
||||
newArray[i] = this._array[this._getCyclicIndex(i)];
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
* @param index The index of the value to get.
|
||||
* @return The value corresponding to the index.
|
||||
*/
|
||||
public get(index: number): T {
|
||||
public get(index: number): T | undefined {
|
||||
return this._array[this._getCyclicIndex(index)];
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
* @param index The index to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
public set(index: number, value: T): void {
|
||||
public set(index: number, value: T | undefined): void {
|
||||
this._array[this._getCyclicIndex(index)] = value;
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ export class CircularList<T> extends EventEmitter implements ICircularList<T> {
|
||||
* Removes and returns the last value on the list.
|
||||
* @return The popped value.
|
||||
*/
|
||||
public pop(): T {
|
||||
public pop(): T | undefined {
|
||||
return this._array[this._getCyclicIndex(this._length-- - 1)];
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@
|
||||
|
||||
import { XtermListener } from './Types';
|
||||
import { IEventEmitter, IDisposable } from 'xterm';
|
||||
import { Disposable } from './Lifecycle';
|
||||
|
||||
export class EventEmitter implements IEventEmitter, IDisposable {
|
||||
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 || {};
|
||||
@@ -26,15 +28,17 @@ export class EventEmitter implements IEventEmitter, IDisposable {
|
||||
* @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);
|
||||
let disposed = false;
|
||||
return {
|
||||
dispose: () => {
|
||||
if (!handler) {
|
||||
if (disposed) {
|
||||
// Already disposed
|
||||
return;
|
||||
}
|
||||
this.off(type, handler);
|
||||
handler = null;
|
||||
disposed = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -44,7 +48,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = this._events[type];
|
||||
const obj = this._events[type];
|
||||
let i = obj.length;
|
||||
|
||||
while (i--) {
|
||||
@@ -65,7 +69,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
|
||||
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);
|
||||
}
|
||||
@@ -76,6 +80,7 @@ export class EventEmitter implements IEventEmitter, IDisposable {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._events = {};
|
||||
}
|
||||
}
|
||||
@@ -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,35 @@
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IEventEmitter } from 'xterm';
|
||||
|
||||
export type XtermListener = (...args: any[]) => void;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export interface ICircularList<T> extends IEventEmitter {
|
||||
length: number;
|
||||
maxLength: number;
|
||||
|
||||
get(index: number): T | undefined;
|
||||
set(index: number, value: T): void;
|
||||
push(value: T): void;
|
||||
pop(): T | undefined;
|
||||
splice(start: number, deleteCount: number, ...items: T[]): void;
|
||||
trimStart(count: number): void;
|
||||
shiftElements(start: number, count: number, offset: number): void;
|
||||
}
|
||||
@@ -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,8 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal, ICircularList, LineData } from '../Types';
|
||||
import { C0 } from '../EscapeSequences';
|
||||
import { ITerminal, IBufferLine } from '../Types';
|
||||
import { ICircularList } from '../common/Types';
|
||||
import { C0 } from '../common/data/EscapeSequences';
|
||||
|
||||
const enum Direction {
|
||||
UP = 'A',
|
||||
@@ -18,7 +19,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 +29,7 @@ export class AltClickHandler {
|
||||
this._startCol = this._terminal.buffer.x;
|
||||
this._startRow = this._terminal.buffer.y;
|
||||
|
||||
let coordinates = this._terminal.mouseHelper.getCoords(
|
||||
const coordinates = this._terminal.mouseHelper.getCoords(
|
||||
this._mouseEvent,
|
||||
this._terminal.element,
|
||||
this._terminal.charMeasure,
|
||||
@@ -50,7 +51,7 @@ export class AltClickHandler {
|
||||
*/
|
||||
public move(): void {
|
||||
if (this._mouseEvent.altKey && this._endCol !== undefined && this._endRow !== undefined) {
|
||||
this._terminal.send(this._arrowSequences());
|
||||
this._terminal.handler(this._arrowSequences());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,10 +90,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()));
|
||||
}
|
||||
@@ -108,8 +109,8 @@ 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,
|
||||
@@ -118,7 +119,7 @@ export class AltClickHandler {
|
||||
}
|
||||
|
||||
private _moveHorizontallyOnly(): string {
|
||||
let direction = this._horizontalDirection();
|
||||
const direction = this._horizontalDirection();
|
||||
return repeat(Math.abs(this._startCol - this._endCol), this._sequence(direction));
|
||||
}
|
||||
|
||||
@@ -132,13 +133,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++;
|
||||
}
|
||||
}
|
||||
@@ -152,12 +153,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;
|
||||
|
||||
@@ -35,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) {
|
||||
@@ -50,15 +50,15 @@ 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 {
|
||||
const dispatchPaste = function(text: string): void {
|
||||
text = prepareTextForTerminal(text);
|
||||
text = bracketTextForPaste(text, term.bracketedPasteMode);
|
||||
term.handler(text);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
|
||||
import BaseCharAtlas from './atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { CHAR_DATA_CHAR_INDEX } from '../Buffer';
|
||||
@@ -22,6 +22,19 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
protected _charAtlas: BaseCharAtlas;
|
||||
|
||||
/**
|
||||
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||
*/
|
||||
private _currentGlyphIdentifier: IGlyphIdentifier = {
|
||||
chars: '',
|
||||
code: 0,
|
||||
bg: 0,
|
||||
fg: 0,
|
||||
bold: false,
|
||||
dim: false,
|
||||
italic: false
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
@@ -36,6 +49,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._container.removeChild(this._canvas);
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
this._ctx = this._canvas.getContext('2d', {alpha: this._alpha});
|
||||
// Draw the background if this is an opaque layer
|
||||
@@ -49,7 +69,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);
|
||||
@@ -224,12 +244,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.
|
||||
@@ -237,33 +257,41 @@ 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, italic: boolean): void {
|
||||
const drawInBrightColor = terminal.options.drawBoldTextInBrightColors && bold && fg < 8;
|
||||
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;
|
||||
this._currentGlyphIdentifier.chars = chars;
|
||||
this._currentGlyphIdentifier.code = code;
|
||||
this._currentGlyphIdentifier.bg = bg;
|
||||
this._currentGlyphIdentifier.fg = fg;
|
||||
this._currentGlyphIdentifier.bold = bold && terminal.options.enableBold;
|
||||
this._currentGlyphIdentifier.dim = dim;
|
||||
this._currentGlyphIdentifier.italic = italic;
|
||||
const atlasDidDraw = this._charAtlas && this._charAtlas.draw(
|
||||
this._ctx,
|
||||
{char, code, bg, fg, bold: bold && terminal.options.enableBold, dim, italic},
|
||||
this._currentGlyphIdentifier,
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop
|
||||
);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChar(terminal, char, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
this._drawUncachedChars(terminal, chars, width, fg, x, y, bold && terminal.options.enableBold, dim, italic);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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, italic: 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, italic);
|
||||
this._ctx.textBaseline = 'top';
|
||||
@@ -285,7 +313,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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -33,10 +33,10 @@ describe('ColorManager', () => {
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (let key of Object.keys(cm.colors)) {
|
||||
for (const key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok(cm.colors[key].css.length >= 7);
|
||||
assert.ok((<any>cm.colors)[key].css.length >= 7);
|
||||
}
|
||||
}
|
||||
assert.equal(cm.colors.ansi.length, 256);
|
||||
|
||||
@@ -42,7 +42,7 @@ export const DEFAULT_ANSI_COLORS = (() => {
|
||||
|
||||
// Fill in the remaining 240 ANSI colors.
|
||||
// Generate colors (16-231)
|
||||
let v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
for (let i = 0; i < 216; i++) {
|
||||
const r = v[(i / 36) % 6 | 0];
|
||||
const g = v[(i / 6) % 6 | 0];
|
||||
@@ -75,7 +75,7 @@ function fromHex(css: string): IColor {
|
||||
}
|
||||
|
||||
function toPaddedHex(c: number): string {
|
||||
let s = c.toString(16);
|
||||
const s = c.toString(16);
|
||||
return s.length < 2 ? '0' + s : s;
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
const charData = terminal.buffer.lines.get(cursorY)[terminal.buffer.x];
|
||||
const charData = terminal.buffer.lines.get(cursorY).get(terminal.buffer.x);
|
||||
if (!charData) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ILinkHoverEvent, ITerminal, ILinkifierAccessor, LinkHoverEventTypes } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkHoverEvent = null;
|
||||
@@ -39,7 +40,15 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkHoverEvent): void {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (e.fg < 256) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
// Single line link
|
||||
this.fillBottomLineAtCells(e.x1, e.y1, e.x2 - e.x1);
|
||||
|
||||
+30
-12
@@ -7,13 +7,14 @@ import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { ColorManager } from './ColorManager';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions } from './Types';
|
||||
import { ITerminal } from '../Types';
|
||||
import { IRenderLayer, IColorSet, IRenderer, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { EventEmitter } from '../EventEmitter';
|
||||
import { RenderDebouncer } from '../utils/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../utils/ScreenDprMonitor';
|
||||
import { EventEmitter } from '../common/EventEmitter';
|
||||
import { RenderDebouncer } from '../ui/RenderDebouncer';
|
||||
import { ScreenDprMonitor } from '../ui/ScreenDprMonitor';
|
||||
import { ITheme } from 'xterm';
|
||||
import { CharacterJoinerRegistry } from '../renderer/CharacterJoinerRegistry';
|
||||
|
||||
export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _renderDebouncer: RenderDebouncer;
|
||||
@@ -23,6 +24,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
public colorManager: ColorManager;
|
||||
public dimensions: IRenderDimensions;
|
||||
@@ -31,12 +33,13 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
super();
|
||||
const allowTransparency = this._terminal.options.allowTransparency;
|
||||
this.colorManager = new ColorManager(document, allowTransparency);
|
||||
this._characterJoinerRegistry = new CharacterJoinerRegistry(_terminal);
|
||||
if (theme) {
|
||||
this.colorManager.setTheme(theme);
|
||||
}
|
||||
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, allowTransparency),
|
||||
new TextRenderLayer(this._terminal.screenElement, 0, this.colorManager.colors, this._characterJoinerRegistry, allowTransparency),
|
||||
new SelectionRenderLayer(this._terminal.screenElement, 1, this.colorManager.colors),
|
||||
new LinkRenderLayer(this._terminal.screenElement, 2, this.colorManager.colors, this._terminal),
|
||||
new CursorRenderLayer(this._terminal.screenElement, 3, this.colorManager.colors)
|
||||
@@ -62,15 +65,22 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this._renderDebouncer = new RenderDebouncer(this._terminal, this._renderRows.bind(this));
|
||||
this._screenDprMonitor = new ScreenDprMonitor();
|
||||
this._screenDprMonitor.setListener(() => this.onWindowResize(window.devicePixelRatio));
|
||||
this.register(this._screenDprMonitor);
|
||||
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in window) {
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), {threshold: 0});
|
||||
const observer = new IntersectionObserver(e => this.onIntersectionChange(e[0]), { threshold: 0 });
|
||||
observer.observe(this._terminal.element);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._renderLayers.forEach(l => l.dispose());
|
||||
}
|
||||
|
||||
public onIntersectionChange(entry: IntersectionObserverEntry): void {
|
||||
this._isPaused = entry.intersectionRatio === 0;
|
||||
if (!this._isPaused && this._needsFullRefresh) {
|
||||
@@ -141,8 +151,8 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this._runOperation(l => l.onFocus(this._terminal));
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number], end: [number, number]): void {
|
||||
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end));
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.onSelectionChanged(this._terminal, start, end, columnSelectMode));
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
@@ -169,8 +179,8 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
/**
|
||||
* Queues a refresh between two rows (inclusive), to be done on next animation
|
||||
* frame.
|
||||
* @param {number} start The start row.
|
||||
* @param {number} end The end row.
|
||||
* @param start The start row.
|
||||
* @param end The end row.
|
||||
*/
|
||||
public refreshRows(start: number, end: number): void {
|
||||
if (this._isPaused) {
|
||||
@@ -186,7 +196,7 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
*/
|
||||
private _renderRows(start: number, end: number): void {
|
||||
this._renderLayers.forEach(l => l.onGridChanged(this._terminal, start, end));
|
||||
this._terminal.emit('refresh', {start, end});
|
||||
this._terminal.emit('refresh', { start, end });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -248,4 +258,12 @@ export class Renderer extends EventEmitter implements IRenderer {
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._terminal.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._terminal.cols;
|
||||
}
|
||||
|
||||
public registerCharacterJoiner(handler: CharacterJoinerHandler): number {
|
||||
return this._characterJoinerRegistry.registerCharacterJoiner(handler);
|
||||
}
|
||||
|
||||
public deregisterCharacterJoiner(joinerId: number): boolean {
|
||||
return this._characterJoinerRegistry.deregisterCharacterJoiner(joinerId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,39 +7,46 @@ import { ITerminal } from '../Types';
|
||||
import { IColorSet, IRenderDimensions } from './Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number];
|
||||
end: [number, number];
|
||||
columnSelectMode: boolean;
|
||||
ydisp: number;
|
||||
}
|
||||
|
||||
export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state: {start: [number, number], end: [number, number]};
|
||||
private _state: ISelectionState;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet) {
|
||||
super(container, 'selection', zIndex, true, colors);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
private _clearState(): void {
|
||||
this._state = {
|
||||
start: null,
|
||||
end: null
|
||||
end: null,
|
||||
columnSelectMode: null,
|
||||
ydisp: null
|
||||
};
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
super.resize(terminal, dim);
|
||||
// Resizing the canvas discards the contents of the canvas so clear state
|
||||
this._state = {
|
||||
start: null,
|
||||
end: null
|
||||
};
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
public reset(terminal: ITerminal): void {
|
||||
if (this._state.start && this._state.end) {
|
||||
this._state = {
|
||||
start: null,
|
||||
end: null
|
||||
};
|
||||
this._clearState();
|
||||
this.clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void {
|
||||
public onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
// Selection has not changed
|
||||
if (this._state.start === start || this._state.end === end) {
|
||||
if (!this._didStateChange(start, end, columnSelectMode, terminal.buffer.ydisp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,25 +69,50 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
this._ctx.fillStyle = this._colors.selection.css;
|
||||
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
||||
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
|
||||
if (columnSelectMode) {
|
||||
const startCol = start[0];
|
||||
const width = end[0] - startCol;
|
||||
const height = viewportCappedEndRow - viewportCappedStartRow + 1;
|
||||
this.fillCells(startCol, viewportCappedStartRow, width, height);
|
||||
} else {
|
||||
// Draw first row
|
||||
const startCol = viewportStartRow === viewportCappedStartRow ? start[0] : 0;
|
||||
const startRowEndCol = viewportCappedStartRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
this.fillCells(startCol, viewportCappedStartRow, startRowEndCol - startCol, 1);
|
||||
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
this.fillCells(0, viewportCappedEndRow, endCol, 1);
|
||||
// Draw middle rows
|
||||
const middleRowsCount = Math.max(viewportCappedEndRow - viewportCappedStartRow - 1, 0);
|
||||
this.fillCells(0, viewportCappedStartRow + 1, terminal.cols, middleRowsCount);
|
||||
|
||||
// Draw final row
|
||||
if (viewportCappedStartRow !== viewportCappedEndRow) {
|
||||
// Only draw viewportEndRow if it's not the same as viewportStartRow
|
||||
const endCol = viewportEndRow === viewportCappedEndRow ? end[0] : terminal.cols;
|
||||
this.fillCells(0, viewportCappedEndRow, endCol, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Save state for next render
|
||||
this._state.start = [start[0], start[1]];
|
||||
this._state.end = [end[0], end[1]];
|
||||
this._state.columnSelectMode = columnSelectMode;
|
||||
this._state.ydisp = terminal.buffer.ydisp;
|
||||
}
|
||||
|
||||
private _didStateChange(start: [number, number], end: [number, number], columnSelectMode: boolean, ydisp: number): boolean {
|
||||
return !this._areCoordinatesEqual(start, this._state.start) ||
|
||||
!this._areCoordinatesEqual(end, this._state.end) ||
|
||||
columnSelectMode !== this._state.columnSelectMode ||
|
||||
ydisp !== this._state.ydisp;
|
||||
}
|
||||
|
||||
private _areCoordinatesEqual(coord1: [number, number], coord2: [number, number]): boolean {
|
||||
if (!coord1 || !coord2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return coord1[0] === coord2[0] && coord1[1] === coord2[1];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions } from './Types';
|
||||
import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, NULL_CELL_CODE } from '../Buffer';
|
||||
import { FLAGS, IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
|
||||
import { CharData, ITerminal } from '../Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from './atlas/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
@@ -22,10 +22,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _characterWidth: number;
|
||||
private _characterFont: string;
|
||||
private _characterOverlapCache: { [key: string]: boolean } = {};
|
||||
private _characterJoinerRegistry: ICharacterJoinerRegistry;
|
||||
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, alpha: boolean) {
|
||||
constructor(container: HTMLElement, zIndex: number, colors: IColorSet, characterJoinerRegistry: ICharacterJoinerRegistry, alpha: boolean) {
|
||||
super(container, 'text', zIndex, alpha, colors);
|
||||
this._state = new GridCache<CharData>();
|
||||
this._characterJoinerRegistry = characterJoinerRegistry;
|
||||
}
|
||||
|
||||
public resize(terminal: ITerminal, dim: IRenderDimensions): void {
|
||||
@@ -52,9 +54,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
terminal: ITerminal,
|
||||
firstRow: number,
|
||||
lastRow: number,
|
||||
joinerRegistry: ICharacterJoinerRegistry | null,
|
||||
callback: (
|
||||
code: number,
|
||||
char: string,
|
||||
chars: string,
|
||||
width: number,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -66,22 +69,54 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
for (let y = firstRow; y <= lastRow; y++) {
|
||||
const row = y + terminal.buffer.ydisp;
|
||||
const line = terminal.buffer.lines.get(row);
|
||||
const joinedRanges = joinerRegistry ? joinerRegistry.getJoinedCharacters(row) : [];
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
const charData = line[x];
|
||||
const code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
const char: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const charData = line.get(x);
|
||||
let code: number = <number>charData[CHAR_DATA_CODE_INDEX];
|
||||
|
||||
// Can either represent character(s) for a single cell or multiple cells
|
||||
// if indicated by a character joiner.
|
||||
let chars: string = charData[CHAR_DATA_CHAR_INDEX];
|
||||
const attr: number = charData[CHAR_DATA_ATTR_INDEX];
|
||||
let width: number = charData[CHAR_DATA_WIDTH_INDEX];
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
let isJoined = false;
|
||||
let lastCharX = x;
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by
|
||||
// the char at x-1
|
||||
if (width === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the right is a
|
||||
// space, take ownership of the cell to the right.
|
||||
if (this._isOverlapping(charData)) {
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
if (joinedRanges.length > 0 && x === joinedRanges[0][0]) {
|
||||
isJoined = true;
|
||||
const range = joinedRanges.shift();
|
||||
|
||||
// We already know the exact start and end column of the joined range,
|
||||
// so we get the string and width representing it directly
|
||||
chars = terminal.buffer.translateBufferLineToString(
|
||||
row,
|
||||
true,
|
||||
range[0],
|
||||
range[1]
|
||||
);
|
||||
width = range[1] - range[0];
|
||||
code = Infinity;
|
||||
|
||||
// Skip over the cells occupied by this range in the loop
|
||||
lastCharX = range[1] - 1;
|
||||
}
|
||||
|
||||
// If the character is an overlapping char and the character to the
|
||||
// right is a space, take ownership of the cell to the right. We skip
|
||||
// this check for joined characters because their rendering likely won't
|
||||
// yield the same result as rendering the last character individually.
|
||||
if (!isJoined && this._isOverlapping(charData)) {
|
||||
// If the character is overlapping, we want to force a re-render on every
|
||||
// frame. This is specifically to work around the case where two
|
||||
// overlaping chars `a` and `b` are adjacent, the cursor is moved to b and a
|
||||
@@ -89,7 +124,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
// get removed, and `a` would not re-render because it thinks it's
|
||||
// already in the correct state.
|
||||
// this._state.cache[x][y] = OVERLAP_OWNED_CHAR_DATA;
|
||||
if (x < line.length - 1 && line[x + 1][CHAR_DATA_CODE_INDEX] === 32 /*' '*/) {
|
||||
if (lastCharX < line.length - 1 && line.get(lastCharX + 1)[CHAR_DATA_CODE_INDEX] === NULL_CELL_CODE) {
|
||||
width = 2;
|
||||
// this._clearChar(x + 1, y);
|
||||
// The overlapping char's char data will force a clear and render when the
|
||||
@@ -116,7 +151,18 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
callback(code, char, width, x, y, fg, bg, flags);
|
||||
callback(
|
||||
code,
|
||||
chars,
|
||||
width,
|
||||
x,
|
||||
y,
|
||||
fg,
|
||||
bg,
|
||||
flags
|
||||
);
|
||||
|
||||
x = lastCharX;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,7 +180,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
ctx.save();
|
||||
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, null, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
// libvte and xterm both draw the background (but not foreground) of invisible characters,
|
||||
// so we should too.
|
||||
let nextFillStyle = null; // null represents default background color
|
||||
@@ -176,7 +222,7 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(terminal: ITerminal, firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(terminal, firstRow, lastRow, (code, char, width, x, y, fg, bg, flags) => {
|
||||
this._forEachCell(terminal, firstRow, lastRow, this._characterJoinerRegistry, (code, chars, width, x, y, fg, bg, flags) => {
|
||||
if (flags & FLAGS.INVISIBLE) {
|
||||
return;
|
||||
}
|
||||
@@ -190,11 +236,11 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
}
|
||||
this.fillBottomLineAtCells(x, y);
|
||||
this.fillBottomLineAtCells(x, y, width);
|
||||
this._ctx.restore();
|
||||
}
|
||||
this.drawChar(
|
||||
terminal, char, code,
|
||||
this.drawChars(
|
||||
terminal, chars, code,
|
||||
width, x, y,
|
||||
fg, bg,
|
||||
!!(flags & FLAGS.BOLD), !!(flags & FLAGS.DIM), !!(flags & FLAGS.ITALIC)
|
||||
@@ -208,7 +254,9 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
return;
|
||||
}
|
||||
|
||||
this._charAtlas.beginFrame();
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.beginFrame();
|
||||
}
|
||||
|
||||
this.clearCells(0, firstRow, terminal.cols, lastRow - firstRow + 1);
|
||||
this._drawBackground(terminal, firstRow, lastRow);
|
||||
|
||||
+34
-6
@@ -3,8 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminal } from '../Types';
|
||||
import { IEventEmitter, ITheme } from 'xterm';
|
||||
import { ITerminal, CharacterJoinerHandler } from '../Types';
|
||||
import { IEventEmitter, ITheme, IDisposable } from 'xterm';
|
||||
import { IColorSet } from '../shared/Types';
|
||||
|
||||
/**
|
||||
@@ -20,21 +20,28 @@ export const enum FLAGS {
|
||||
ITALIC = 64
|
||||
}
|
||||
|
||||
export interface IRenderer extends IEventEmitter {
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IEventEmitter, IDisposable {
|
||||
dimensions: IRenderDimensions;
|
||||
colorManager: IColorManager;
|
||||
|
||||
dispose(): void;
|
||||
setTheme(theme: ITheme): IColorSet;
|
||||
onWindowResize(devicePixelRatio: number): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number], end: [number, number]): void;
|
||||
onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
refreshRows(start: number, end: number): void;
|
||||
registerCharacterJoiner(handler: CharacterJoinerHandler): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
@@ -59,7 +66,7 @@ export interface IRenderDimensions {
|
||||
actualCellHeight: number;
|
||||
}
|
||||
|
||||
export interface IRenderLayer {
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
@@ -94,7 +101,17 @@ export interface IRenderLayer {
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number]): void;
|
||||
onSelectionChanged(terminal: ITerminal, start: [number, number], end: [number, number], columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
*/
|
||||
registerCharacterJoiner?(joiner: ICharacterJoiner): void;
|
||||
|
||||
/**
|
||||
* Deregisters the specified character joiner handler
|
||||
*/
|
||||
deregisterCharacterJoiner?(joinerId: number): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
@@ -106,3 +123,14 @@ export interface IRenderLayer {
|
||||
*/
|
||||
reset(terminal: ITerminal): void;
|
||||
}
|
||||
|
||||
export interface ICharacterJoiner {
|
||||
id: number;
|
||||
handler: CharacterJoinerHandler;
|
||||
}
|
||||
|
||||
export interface ICharacterJoinerRegistry {
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user